ADA 9X MAPPING VOLUME I MAPPING RATIONALE Version 4.1 5 March 1992 IR-MA-1249-2 Ada 9X Mapping/Revision Team Intermetrics, Inc. 733 Concord Avenue Cambridge, Massachusetts 02138 (617)661-1840 Published by Intermetrics, Inc. 733 Concord Avenue Cambridge, Massachusetts 02138 Reprinting permitted if accompanied by this statement. Copyright (C) 1992 Intermetrics, Inc. This work is sponsored by the Ada 9X Project Office under contract F08635-90-C-0066. Foreword There are two volumes of the Ada 9X Mapping Document: Volume I, the Mapping Rationale and Volume II, the Mapping Specification. Volume I, the Mapping Rationale, addresses why specific language changes are being recommended to satisfy the Ada 9X requirements (see Ada 9X Requirements, DoD, December 1990). Volume II, the Mapping Specification, contains a detailed description of the key language changes that are being recommended along with a traceability index between proposed changes and requirements. This release of the Mapping Document (March '92) revises the Specialized Needs Annexes (including additional rationale), and provides an update to the rationale. Volume I, the Mapping Rationale, has been revised to reflect public comments received on the previous version, and to reflect the changes to the core language as described in Volume II, the Mapping Specification (December '91). Volume I now contains the section-by-section rationale for Chapters 1 through 14 of the Mapping Specification (Appendix S), and an analysis of Ada 9X upward compatibility/consistency (Appendix U). Volume II, the Mapping Specification, is abridged for this release (March '92). Chapters 1 through Chapters 14 are not included because there were no changes made in these chapters since December '91. Also, the six areas contained in the Specialized Needs Annex now appear as six individual annexes. The rationale for the six annexes is included. These six individual annexes are: - Systems Programming (Annex G) - Real-Time Systems (Annex H) - Distributed Systems (Annex I) - Information Systems (Annex J) - Safety and Security (Annex K) - Numerics (Annex L) The specifications in the Systems Programming, Real-Time Systems, and Distributed Systems annexes are identical to that of Volume II, the Mapping Specification (December '91). There was a minor nomenclature change in the Distribution annex. The term ``datagram'' has been replaced with the term ``asynchronous''. The changes in the Information Systems Annex and the Numerics Annex are a result of further comments from team members and the public. It is expected that all of the annexes will continue to evolve over time. As always, your comments on this draft are welcome and should be received by 1 May 92 to have the greatest impact. Please follow the instructions for comment submission located on the following page. Christine M. Anderson Ada 9X Project Manager WL/MNAG Eglin AFB, FL 32542-5434 TEL: (904)882-8264 FAX: (904)882-2095 e-mail: anderson@uv4.eglin.af.mil Instructions for Comment Submission Comments should be sent by 1 May 92 via one of the following methods: U.S.Mail: Ada 9X Mapping/Revision Team Intermetrics, Inc. 733 Concord Avenue Cambridge, MA 02138 Phone: (617) 661-1840 FAX: (617)868-2843 Attn: Ada 9X Mapping/Revision Team E-Mail: ada9x-mrt@inmet.com Please note that the E-mail address for all correspondence to the Ada 9X Mapping/Revision team has been changed. Comments should use the following format: !topic Title summarizing comment on Mapping Rationale !reference MR-ss.ss; 4.1 !from Author Name yy-mm-dd !keywords keywords related to topic !discussion where ss.ss is the section number of the document, and yy-mm-dd is the date the comment was sent. The date is optional if sent via e-mail. If possible please send comments by e-mail. Please send one message per section or comment, to facilitate cross-referencing. If your comments are lengthy and you cannot send them by e-mail, it would be appreciated if you could include a copy in machine-readable form, using either a Macintosh or IBM compatible disk format. All comments sent via E-mail should receive a confirmation message from the mapping team. If a confirmation is not received we ask that the sender try sending the message one more time. In the event that the second E-mail attempt is unsuccessful, the sender is then asked to call Intermetrics at the phone number noted above. Thank you for your help. Acknowledgements This document was prepared by the Ada 9X Mapping/Revision Team based at Intermetrics, Inc. The members of the team are: W. Carlson, Program Manager; T. Taft, Technical Director; C. Garrity; R. Duff (Oak Tree Software); R. Hilliard; O. Pazy; D. Rosenfeld; L. Shafer; W. White. The following consultants to the Ada 9X Project have contributed to the Specialized Needs Annexes: T. Baker (Real-Time/Systems-Programming -- SEI, FSU); B. Brosgol (Information-Systems -- Consultant); K. Dritz (Numerics -- Argonne National Laboratory); A. Gargaro (Distribution -- Computer Sciences); J. Goodenough (Real-Time/Systems-Programming -- SEI); J. McHugh (Safety- Critical -- Consultant); B. Wichmann (Safety-Critical -- NPL: UK). This work is continuously being reviewed by the Ada 9X Distinguished Reviewers: E. Ploedereder, Chairman (Tartan); B. Bardin (Hughes); B. Brett (DEC); B. Brosgol (Consultant); N. Cohen (IBM); R. Dewar (NYU); G. Dismukes (Telesoft); A. Evans (Consultant); A. Gargaro (Computer Sciences); M. Gerhardt (ESL); J. Goodenough (SEI); T. Harrison (ISSI); S. Heilbrunner (University of Salzburg: Austria); P. Hilfinger (UC/Berkeley); J. Ichbiah (Consultant: DR emeritus); M. Kamrad II (Paramax Systems); P. Kruchten (Rational); R. Landwehr (CCI: Germany); C. Lester (Portsmouth Polytechnic: UK); D. Luckham (Stanford); L. Mansson (TELIA Research: Sweden); R. Mathis (Consultant); S. Michell (Multiprocessor Toolsmiths: Canada); M. Mills (US Air Force); D. Pogge (US Navy); K. Power (Boeing); O. Roubine (Verdix: France); W. Taylor (Consultant: UK); E. Vasilescu (Grumman). Other valuable feedback influencing the revision process is also continuously being received from the Ada 9X Language Precision Team (Odyssey Research Associates), the Ada 9X User/Implementor Teams (AETECH, Tartan, Telesoft), the Ada 9X Implementation Analysis Team (New York University) and the Ada community-at-large. The Ada 9X Project is sponsored by the Ada Joint Program Office. Christine M. Anderson at the Air Force Wright Laboratory, Armament Directorate, is the project manager. 1. Introduction Ada is a strongly typed language for implementing large, complex software systems, especially real-time and embedded systems. It was developed by an international design team in response to requirements issued by the United States government. The Ada 9X program is preparing a refined version of Ada to update the Ada standard in accordance with ANSI and ISO procedures. This volume of the Ada 9X Mapping provides the rationale for refinements proposed for Ada as part of the Ada 9X Mapping/Revision process. (Volume II, the Mapping Specification, describes the proposed changes in detail.) The Ada 9X Mapping/Revision effort is an ongoing process, and proposals discussed here continue to undergo review and refinement. All of the proposals are in a near-final state, though continued simplification of specific rules or specific features is anticipated. Furthermore, some mapping proposals may not ultimately be adopted, in keeping with the goal of minimizing disturbance to the existing Ada community, while satisfying the critical revision requirements. 1.1. Background Ada became an ANSI standard in 1983 and an ISO standard in 1987. Although it was originally designed to support real-time/embedded systems, its domain has expanded to include many other application areas, such as large-scale information systems, scientific computation, and systems programming in diverse environments. After years of use, and numerous implementations, the determination was made in 1988 to undertake a revision to Ada, leading to an updated ANSI/ISO standard. The effort began in January 1988 when the Ada Board was asked to prepare a recommendation to the Ada Joint Program Office (AJPO) on the most appropriate standardization process to use in developing a revised Ada standard, known as Ada 9X. The recommendation [DoD 88] was delivered in September 1988 to Virginia Castor, Director, Ada Joint Program Office, who subsequently established the Ada 9X Project for conducting the revision of the Ada Standard. Christine M. Anderson was appointed Project Manager in October 1988. The Ada 9X effort consists of several phases. The output of the Requirements Definition phase was the Ada 9X Requirements document [DoD 90], which specifies the revision needs to be addressed. The Mapping/Revision phase is to define changes to the standard to meet those requirements. The requirements definition effort and the mapping/revision efforts are both constrained by the overall objective of the Ada 9X effort [DoD 89a]: Revise ANSI/MIL-STD-1815A-1983 to reflect current essential requirements with minimum negative impact and maximum positive impact to the Ada community. 1.2. Approach In responding to the revision requirements, the Mapping/Revision team has followed the inspiration of Jean Ichbiah (who led the original design team), both in remaining faithful to the principles underlying the original Ada design, and in the approach to the revision process. To quote Dr. Ichbiah in his introduction to John Barnes' textbook on Ada [Barnes 81, vii]: Clearly, further progress can only come by a reappraisal of implicit assumptions underlying certain compromises. Here is the major contradiction in any design work. On the one hand, one can only reach an harmonious integration of several features by immersing oneself into the logic of the existing parts; it is only in this way that one can achieve a perfect combination. On the other hand, this perception of perfection, and the implied acceptance of certain unconscious assumptions, will prevent further progress. The enhanced functionality in Ada 9X is achieved by generalizing existing Ada features, and by removing special cases and restrictions. We made a careful analysis of Ada, and of language study notes prepared during the original design process. Based on this analysis, and on the Ada community's experience in implementing and using Ada during the past ten years, we identified limitations that, while they were included to simplify implementations and/or to lower risk when the language was first standardized, are no longer necessary. We also drew upon the wealth of practical experience gained during the 1980s in the use of object-oriented design methods, object-oriented programming languages, and real-time programming made possible by Ada. The proposed Ada 9X revision is upwardly compatible for most existing Ada 83 applications. Most incompatibilities are restricted to combinations of features that are rarely used in practice. (A thorough analysis of the upward incompatibilities is provided in Appendix U of this document.) As with Ada 83, there is a ``core'' language, which must be implemented in its entirety. In Ada 9X, several Specialized Needs Annexes are defined, which provide extended features for specific application areas. These Annexes provide standard definitions for application-specific packages, pragmas, type attributes, and capacity and performance characteristics of implementations. Application- specific requirements are defined for the following areas: Systems Programming, Real-Time Systems, Distributed Systems, Information Systems, Safety and Security, and Numerics. Most, but not all, of the functionality defined in these Annexes is intended to be optional. Chapter 2 provides an overview of Ada, to give experienced programmers a framework within which to understand the individual features and their uses. For the most part, this overview is equally applicable to both Ada 83 and to Ada 9X. Areas of enhanced functionality in Ada 9X are highlighted. Later sections of this document describe the need for these enhancements. Chapter 3 of this Rationale demonstrates, through a series of examples, how each of the Ada 9X requirements has been addressed and that each change is motivated by one or more requirements. Chapter 4 addresses the three major new facilities in Ada 9X: object-oriented programming; child library units; and data synchronization with protected records. We recommend that you read this Rationale with the Ada 83 Language Reference Manual (RM), the Ada 9X Mapping, volume II, Mapping Specification (MS, version 4.0 for the core, and version 4.1 for the annexes) at hand since frequent reference is made to these documents. References to the Ada 83 Language Reference Manual take the form: RM ss.ss(pp) where ss.ss is the section of the document and pp is the paragraph number where applicable. References to the Ada 9X Mapping Specification ``core'' take the form: MS-ss.ss(pp) where the same numbering convention is used. References to the Mapping Specification Annexes take the form: MS-X where X is G, H, I, J, or K. It would also be helpful to have a copy of the Ada 9X Requirements [DoD 90] as it describes the user needs that motivate each of the requirements. 2. Overview of the Ada Language This chapter gives an overview of the Ada language. This overview is included in the Rationale to illustrate how Ada 9X is a natural evolutionary enhancement of Ada 83. This overview also provides a conceptual framework within which to understand the detailed discussions which follow. Ada is a modern algorithmic language with the usual control structures, and with the ability to define types and subprograms. It also serves the need for modularity, whereby data, types and subprograms can be packaged. It treats modularity in the physical sense as well, with a facility to support separate compilation.[Throughout this document we paraphrase the Ada Reference Manual [ANSI 83] without attribution.] In addition to these aspects, the language supports real-time programming, with facilities to define the invocation, synchronization, and timing of parallel tasks. It also supports systems programming, with facilities that allow access to system-dependent properties, and precise control over the representation of data. In this chapter, areas of enhanced functionality in the proposed 9X version of Ada are indicated with underlining. The fact that these limited and predominantly upward compatible enhancements to Ada allow it to support state-of-the-art programming in the nineties and beyond, reconfirms the validity of Ada's underlying principles, and is a proof of the excellence of the original design. The remainder of this chapter is organized as follows: Section 2.1 discusses the type model, and shows how it supports object-oriented programming. Section 2.2 discusses the primary syntactic building blocks of the language, and how they are executed at run time. Section 2.3 discusses how Ada programs are assembled from components stored in a library. Section 2.4 discusses Ada's support for concurrent processing. Section 2.5 discusses the handling of run-time errors (exceptions). Section 2.6 discusses the predefined input-output packages. Section 2.7 discusses features that allow access to and control of low-level machine characteristics. Section 2.8 discusses the Specialized Needs Annexes. 2.1. Objects, Types, Classes and Operations Ada was an important milestone in the development of object-oriented design. This section describes two fundamental concepts of Ada: types, which determine a set of values with associated operations, and objects, which are instances of those types. Objects hold values. Variables are objects whose values can be changed; constants are objects whose values cannot be changed. 2.1.1. Objects and Their Types Every object has an associated type. The type determines a set of possible values that the object can contain, and the operations that can be applied to it. Users write declarations to define new types and objects. Ada is a block-structured language in which the scope of declarations, including object and type declarations, is static. Static scoping means that the visibility of names does not depend on the input data when the program is run, but only on the textual structure of the program. Static properties such as visibility can be changed only by modifying and recompiling the source code. Objects may be created when the executing program enters the scope where they are declared (elaboration); they are deleted when the execution leaves that scope (finalization). In addition, allocators are executable operations that create objects dynamically. An allocator produces an access value (a value of an access type), which provides access to the dynamically created object. An access value is said to designate an object. Access objects are only allowed to designate objects of the type specified by the access type. Access types correspond to pointer types or references in other programming languages. A base type, together with a constraint, forms a subtype. User-defined subtypes constrain the values of the subtype to a subset of the values of the base type. Subtype constraints are useful for run-time error detection, because they show the programmer's intent. Subtypes may also allow an optimizing compiler to make more efficient use of hardware resources, because they give the compiler more information about the behavior of the program. User-defined types provide a finer classification of objects than the predefined types, and hence greater assurance that operations are applied to only those objects for which the operations are meaningful. 2.1.2. Type Classes Ada 83 groups its predefined types into classes such as scalar, array, record, private and task, and used this classification to simplify the description of language rules (e.g., matching generic parameters, RM 12.3.3). Ada 9X formalizes the notion of class and allows users to define new classes. A class hierarchy of types can be defined by the programmer, and class-wide characteristics of types, their representation and operations, can be defined once without redundancy. Figure 2-1 provides a hierarchical view of the predefined classes. The predefined class hierarchy is used to describe general properties of types: Some attributes and operations apply to any object, some to any elementary object, some to any scalar object, some to any discrete object, and so on: Figure 2-1: Ada Class Hierarchy - The term elementary type is introduced to encompass scalar and access types. The term composite type is broadened to include private types. The use of these terms in Ada 9X serve to regularize this hierarchical classification of types, and thereby simplify the presentation of language rules. - An enumeration type defines an ordered set of distinct enumeration literals, for example a list of states or an alphabet of characters. The enumeration types BOOLEAN, CHARACTER (the 8-bit ISO standard character set) and WIDE_CHARACTER (a 16-bit character type) are predefined. - The numeric class is not strictly a part of the hierarchy, but there are certain properties that are common to all numeric types (such as the availability of arithmetic operations), so we have shown it in this figure via a shaded oval surrounding the numeric types. Types in this class provide a means of performing approximate or exact numerical computations. Approximate computations may be performed using either fixed point types, with absolute bounds on the error, or floating point types, with relative error bounds. Exact computations may be performed with either integer types, which denote sets of consecutive integers, or decimal fixed types, which denote sets of scaled decimal numbers. In Ada 9X, decimal types are implemented as a subclass of the fixed class, with an exact scale. The numeric types INTEGER, FLOAT and DURATION are predefined. - Array and record types allow definitions of structured objects with related components. An array is an object with indexed components, each of which has the same subtype. A record is an object with named components of possibly different types. The array types STRING and WIDE_STRING are predefined. - Access types allow the construction of linked data structures created by the evaluation of allocators. They allow several variables of an access type to designate the same object, and components of one object to designate the same or other objects. Both the elements in such a linked data structure and their relation to other elements can be altered during program execution. Access types may also be used to designate subprograms, stack-allocated variables and constants. - Private types can be defined in packages. Only the logically necessary properties (operations, objects, constants or discriminants) are made accessible to the users of such types. The implementation details of private types are concealed within the package. - Tagged types are extensible record or private types. - Protected record and task types are used in multitasking applications. For each predefined class, Ada provides a special syntax for defining new types within the class. Examples of type declarations: type DISPLAY_COLOR is -- an enumeration type (RED, ORANGE, YELLOW, GREEN, BLUE, VIOLET); type COLOR_MASK is -- an array type array(DISPLAY_COLOR) of BOOLEAN; type MONEY is -- a decimal fixed type delta 0.01 digits 18; type DATE is private; -- a private type type PAYMENT is -- a record type record AMOUNT : MONEY; DUE_DATE : DATE; PAID : BOOLEAN; end record; task type DEVICE is -- a task type entry RESET; end DEVICE; protected type SEMAPHORE is -- a protected type procedure RELEASE; entry SEIZE; private record null; end SEMAPHORE; type DEV is access DEVICE; -- an access type Any composite type may contain special components called discriminants.(In Ada 83 only record types and private types implemented as record types allowed discriminants.) Discriminants provide a method for parameterizing types. A discriminant of a discrete type may be used to control the structure or size of a composite object. More generally, an access discriminant may be used to parameterize a limited type with a reference to an object of another type.(Ada 83 required all discriminants to be of a discrete type.) A type tag is a special kind of discriminant, which identifies a tagged object's type at run time. Record types and private types implemented as records may be tagged. Users may define new types by deriving from any existing type. We say that the new type has been derived from a parent type. When deriving from a tagged type, the user may extend the type by adding new components, including discriminants. For any kind of type, the user may extend the type with additional operations. Types declared either via derivation or via one of the kinds of type definition described above are called specific types. (Specific types correspond exactly to the types of Ada 83; the additional terminology is introduced to distinguish them from class-wide types, which we now discuss.) All of the types derived directly or indirectly from a specific type, together with the type itself, form a user-defined class. The types comprising the class share certain commonalities which a programmer may exploit by utilizing the class-wide type defined for the class. We call this ``class-wide programming.'' This class-wide type is declared implicitly whenever a specific type is defined. The set of values of the class-wide type is the union of the sets of values of the types in the class. If the type is tagged, then objects of the class-wide type are discriminated at run time by their type tags. If the type is not tagged, then all typing information is determined at compile time. The attribute T'CLASS denotes the class-wide type associated with a type T. It may be used for declaring ``class-wide objects,'' access types designating objects in the class, and class-wide operations. We say a type T1 includes another type T2, if the set of values of T1 includes the set of values of T2. A specific type includes only itself. A class-wide type T'CLASS includes the derivatives of T and their class-wide types. Figure 2-2 depicts these relationships graphically. Figure 2-2: A User-Defined Class Hierarchy Since each derived type has exactly one parent, a class is hierarchically structured as a tree. This provides what is termed ``single-inheritance'' in the object-oriented programming community. Tagged types may be extended during derivation. The power of object-oriented programming in part arises from this ability to reuse a parent type's properties via inheritance while selectively overriding that inheritance or defining additional properties. A tagged type declaration looks like this: type ANIMAL is tagged record SPECIES : SPECIES_NAME; WEIGHT : GRAMS; end record; function IMAGE(A : ANIMAL) return STRING; -- Returns a human-readable identification of an ANIMAL This type could be extended as follows: type MAMMAL is new ANIMAL with record HAIR_COLOR : COLOR_ENUM; end record; function IMAGE(M : MAMMAL) return STRING; -- Returns a human-readable identification of a MAMMAL The type MAMMAL has all the components of ANIMAL and adds one to describe the color of the mammal's hair. We may then model primates as mammals with binocular vision, hands, etc.: type PRIMATE is new MAMMAL with record HAND_SIZE : CENTIMETERS; end record; function IMAGE(P : PRIMATE) return STRING; -- Returns a human-readable identification of a PRIMATE 2.1.3. Operations and Overloading There is a set of operations associated with each type. An operation is associated with a type if it takes parameters of the type, or returns a result of the type. Some operations are implicitly declared when a type is defined; others are explicitly declared by the user. A set of operations is implicitly declared immediately after each type declaration. Which operations are implicitly declared depends on the type, and may include any of the following: - Basic operations, such as assignment, component selection, literals and attributes. Basic operations use special syntax specific to the operation. For example, an attribute looks like this: ``X'ATTR'', where X is the name of an entity, and ATTR is the name of an attribute of that entity. For example, ``INTEGER'FIRST'' yields the lower bound of the type INTEGER. - Predefined operators. These are taken from this set of 21 operator symbols: * Logical operators: and or xor * Relational operators: = /= < <= > >= * Binary adding operators: + - & * Unary adding operators: + - * Multiplying operators: * / mod rem * Highest precedence operators: ** abs not - Enumeration literals. - Derived operations, which are inherited by a derived type from its parent type (as explained below). The explicitly declared operations of the type are the subprograms that are explicitly declared to take a parameter of the type, or to return a result of the type. There are two kinds of subprograms: procedures and functions. A procedure defines a sequence of actions; a procedure call is a statement that invokes those actions. A function is like a procedure, but also returns a result; a function call is an expression that produces the result when evaluated. Subprogram calls may be indirect, through an access value. Procedures may take parameters of mode in, which allows reading of the parameters, mode out, which allows writing of the parameters, and mode in out, which allows both reading and writing. All function parameters are of mode in. An in parameter may be an access parameter. Within the subprogram, an access parameter may be dereferenced to allow reading and writing of the designated object (see 2.1). The primitive operations of a type are the implicitly declared operations of the type, and those explicitly defined subprograms of the type that are declared immediately within the same list of declarations as the type itself. A derived type inherits the primitive operations of its parent type, which may then be overridden.(The term ``primitive operation'' is new to Ada 9X; inherited operations were called derived operations in Ada 83, and were limited to those declared in the visible part of a package. Ada 9X generalizes this to all those operations declared in the same list of declarations.) The name of an explicitly declared subprogram is a designator, that is, an identifier or an operator symbol (see the list of operator symbols above). Operators are a syntactic convenience; the operator notation is always equivalent to a function call. For example, ``X + 1'' is equivalent to ``"+"(X, 1)''. When defining new types, users can supply their own implementations for any or all of these operators, and use the new operators in expressions in the same way that predefined operators are used. The benefits of being able to use the designator "+" to refer to the addition function for every numeric type are apparent: The alternative of having to create a unique name for each type's addition function would be cumbersome at best. The operators are just one example of overloading, where a single designator (e.g., "+") is used as the name for more than one entity. Another example of overloading occurs whenever a new type is derived from an existing one. The new type inherits operations from the parent type, including the designators by which the operations are named; these designators are overloaded on the old and new types. Finally, the user may introduce overloading simply by defining several subprograms that have the same name. Overloaded subprograms must be distinguishable by their parameter and result types. Ada compilers must determine a unique meaning for every designator in a program. The process of making this determination is called overload resolution. The compiler uses the context in which the designator is used, including the parameter and result types of subprograms, to perform the overload resolution. If a designator cannot be resolved to a single meaning, then the program is illegal; the user may fix the error by specifying the types of subexpressions explicitly. Many attributes are defined by the language. Attributes denote basic operations, often defined for various classes of Ada's type hierarchy. In some cases, attributes are user-specifiable via an attribute definition clause, allowing users to specify a property of an entity that would otherwise be chosen by default. For example, the ability to read and write values of a type from an external medium is provided by the operations T'READ and T'WRITE. A user may override the default implementations of these operations as follows: Example of User-Specifiable Attribute: type MATRIX is ... for MATRIX'READ use MY_MATRIX_READER; for MATRIX'WRITE use MY_MATRIX_WRITER; 2.1.4. Class-Wide Types and Dispatching Associated with a class, whether user-defined or language-defined, there may be a class-wide type for that class. For example, root_integer'CLASS is the class-wide type associated with the integer class.(root_integer'CLASS was known as universal_integer in Ada 83.) Class-wide types have no operations of their own. However, users may define explicit operations on class-wide types. For example: procedure PRINT(A : in ANIMAL'CLASS); -- Print human-readable information about an ANIMAL The procedure PRINT may be applied to any object within the user-defined class of animals described above. A programmer can define several operations having the same name, even though each operation has a different implementation. The ability to give distinct operations the same name can be used to indicate that these operations have similar, or related, semantics. When the intended operation can be determined at compile time, based on its parameter and result types, overloading of subprogram names is used. For example, the predefined package TEXT_IO contains many operations called PUT, all of which write a value of some type to a file. The implementation of PUT is different for different types. Dispatching provides run-time selection of the proper implementation in situations where the type of an argument to an operation cannot be determined until the program is executed, and in fact might be different each time the operation is invoked. Ada 9X provides dispatching on the primitive operations of tagged types. When a primitive operation on a tagged type is called, the appropriate implementation is chosen based on the tag of the actual value. Typically, this is done at run time; providing a powerful programming technique, known as run-time polymorphism. However, in some cases, the tag can be determined at compile time, which retains the same flexibility while permitting a degree of optimization. Continuing the example from above, we demonstrate both overloading and dispatching: procedure PRINT(S : in STRING); -- Print a string procedure PRINT(A : in ANIMAL'CLASS) is -- Print information on an animal begin PRINT(IMAGE(A)); end PRINT; The PRINT operation is overloaded. One version is defined for STRING and a second is defined for ANIMAL'CLASS. The call to PRINT within the second version resolves at compile time to the version of PRINT defined on STRING (because IMAGE returns a STRING). IMAGE is a dispatching operation: depending on the tag of A the version of IMAGE associated with ANIMAL, MAMMAL, or PRIMATE will be called. 2.1.5. Abstraction and Static Evaluation The emphasis on high performance in Ada applications, and the requirement to support interfacing to special hardware devices, mean that Ada programmers must be able to engineer the low-level mapping of algorithms and data structures onto physical hardware. On the other hand, to build large systems, programmers must operate at a high level of abstraction and compose systems from understandable building blocks. The Ada type system and facilities for separate compilation are ideally suited to reconciling these seemingly conflicting requirements. Ada's support for static checking and evaluation make it a powerful tool, both for the abstract specification of algorithms, and for low-level systems programming and the coding of hardware-dependent algorithms. By static, we mean computations whose results can be determined by analyzing the source code without knowing the values of input data or any other environmental parameters that can change between executions of the program. Ada requires static type checking. The ``scope'' (applicability or lifetime) of declarations is determined by the source code. Careful attention is given to when the sizes of objects are determined. Some objects' sizes are static, and other objects' sizes are not known until run time, but are fixed when the objects are created. The size of an object is only allowed to change during program execution if the object's size depends on discriminant values, and the discriminants have a default value. Other variable-size data structures can be created using dynamically allocated objects and access types. Ada supports users who want to express their algorithms at the abstract level and depend on the compiler to choose efficient implementations, as well as users who need to specify implementation details but also want to declare the associated abstractions to the compiler to facilitate checking during both initial development and maintenance. 2.2. Statements, Expressions and Elaboration Statements are executed at run time to cause an action to occur. Expressions are evaluated at run time to produce a value of some type. Names are also evaluated at run time in the general case; names refer to objects (containing values) or to other entities such as subprograms and types. Declarations are elaborated at run time to produce a new entity with a given name. Many expressions and subtype constraints are statically known; the Ada compiler is required to evaluate static expressions and static subtypes at compile time. It is common that all information about a declaration is known at compile time; in such cases, the run-time elaboration need not actually execute any machine code. The Specialized Needs Annex defines a mechanism that allows Ada compilers to preelaborate certain kinds of units; i.e., the actual actions needed to do the elaboration are done once before the program is ever run instead of many times, each time it is run. 2.2.1. Declarative Parts Several constructs of the language contain a declarative part followed by a sequence of statements. For example, a procedure body looks like this: procedure P(...) is I : INTEGER := 1; -- this is the declarative part ... begin ... -- this is the statement list I := I * 2; ... end P; The execution of the procedure body first elaborates all of the declarations given in the declarative part in the order given. It then executes the list of statements in the order given (unless a transfer of control causes execution to go somewhere other than to the next statement in the list). The effect of elaborating the declarations is to cause the declared entities to come into existence, and to perform other declaration-specific actions. For example, the elaboration of a variable declaration may initialize the variable to the value of some expression. Often, such expressions are evaluated at compile time. However, if the declarations contain non-static expressions, then the elaboration will need to evaluate those expressions at run time. Controlled types allow programmers a means to define what happens to objects at the beginning and end of their lifetimes. For such types, the programmer may define an initialization operation, to be automatically invoked when an object of the type is elaborated, and a finalization operation to be automatically invoked when the object becomes inaccessible. (Declared objects become inaccessible when their scope is left. Objects created by allocators become inaccessible when UNCHECKED_DEALLOCATION is called, or when the scope of the access type is left.) Controlled types provide a means to reliably program dynamic data structures, prevent storage leakage, and leave resources in a consistent state. 2.2.2. Assignments and Control Structures An assignment statement changes the value of a variable. Case statements and if statements allow selection of an enclosed sequence of statements based on the value of an expression. The loop statement allows an enclosed sequence of statements to be executed repeatedly, as directed by an iteration scheme, or until an exit statement is encountered. A goto statement transfers control to a place marked with a label. Additional control mechanisms associated with multitasking and exception handling are discussed below (see 2.4 and 2.5). 2.2.3. Expressions Expressions may appear in many contexts, within both declarations and statements. Expressions are similar to expressions in most programming languages: they may refer to variables, constants and literals, and they may use any of the value-returning operations described in section 2.1.3. An expression produces a value. Every expression has a type that is known at compile time. 2.3. System Construction Ada was designed specifically to support the construction of large, complex, software systems. Therefore, it must allow the composition of programs from small, understandable building blocks, while still allowing programmers to engineer the low-level mapping of algorithms and data structures onto physical hardware. Ada provides support for modern software development techniques with the following capabilities: - Packaging, the grouping together of logically related entities into packages. - Information hiding, where the programmer defines the interface of a program unit for the users of that unit, and separately defines the implementation of the unit. - Object-oriented programming, where objects can be defined in terms of preexisting objects, overload resolution can be used to select operations at compile time, and dispatching can be used to select operations at run time. - Construction of software systems from large numbers of separately compiled units stored in a library, with full compile-time checking of interfaces between separately compiled units. - Class-wide programming. - Construction of multi-lingual systems, programs written in more than one language. The following subsections describe this support in more detail. 2.3.1. Program Units Ada programs are composed of the following kinds of program units: - Subprograms -- functions and procedures. - Packages -- groups of logically related entities. - Generic units -- parameterized templates for subprograms and packages. - Tasks -- active entities that may run in parallel with each other. - Protected records -- passive entities that protect data objects shared by multiple tasks. Program units may be nested within each other, in the same way as in other block-structured languages. Furthermore, they may be separately compiled. Each program unit may be given in two parts: The specification defines the interface between the unit and its users (``clients''). The body defines the implementation of the unit; users do not depend on the implementation details. For packages, the specification consists of the visible part and the private part. The visible part defines the logical interface to the package. The private part defines the physical interface to the package, which is needed to generate efficient code, but has no affect on the logical properties of the entities exported by the package. Thus, the private part may be thought of as part of the implementation of the package, although it is syntactically part of the specification in order to ease the generation of efficient code. Here is what the parts of a package look like: -- This is a package specification: package EXAMPLE is -- This is the visible part -- Declarations of exported entities appear here type COUNT is private; function DOUBLE (C : COUNT) return COUNT; private -- This is the private part -- Declarations appearing here are not exported type COUNT is range 0 .. 100; end EXAMPLE; -- This is the package body: package body EXAMPLE is -- Implementations of exported subprograms appear here -- Entities that are used only in the implementation -- are also declared here ZERO : constant COUNT := 0; -- declaration of constant only used in the body function DOUBLE (C : COUNT) return COUNT is begin return C * 2; end DOUBLE; end EXAMPLE; 2.3.2. Private Types and Information Hiding Packages support information hiding in the sense that users of the package cannot depend on the implementation details that appear in the package body. Private types provide additional information-hiding capability. By declaring a type and its operations in the visible part of a package specification, the user can create a new abstract data type. When a type is declared in a package specification, its implementation details may be hidden by declaring the type to be private. The implementation details are given later, as a full type declaration in the private part of the package. The user of a private type is not allowed to use information about the full type. Users may declare objects of the private type, use the assignment and equality operations, and use any operations declared as subprograms in the visible part of the package. The private type declaration may allow users to refer to discriminants of the type, or it may keep them hidden. A private type may also be declared as limited, in which case even assignment and predefined equality operations are not available (although the programmer may define an equality operation and export it from the package where the type is declared). In the private part, in the body of the package, and in the specifications and bodies of child library units (see 2.3.6), the type is not private: all operations of the type may be used in these places. For example, if the full type declaration declares an array type, then outside users of the type are not allowed to index array components, because these implementation details are hidden. However, code in the package body is allowed the complete set of array operations, because it can see the full type declaration. 2.3.3. Object-Oriented Programming Modern software development practices call for building programs from reusable parts, and for extending existing systems. Ada supports such practices through object-oriented programming features. The basic principle of object-oriented programming is to be able to define one part of a program as an extension of another, pre-existing, part. The basic building blocks of object-oriented programming were discussed in section 2.1. Abstract data types may be defined in Ada using packages and private types. Types may be extended by adding new packages, deriving from existing types, and adding new operations to derived types. For non-tagged types, such extension is ``static'' in the sense that the compiler determines which operations apply to which objects according to the typing and overloading rules. For tagged types, however, operation selection is determined at run time, using the tag carried by each such object. This allows easy extension of existing types: To add a new tagged type, the programmer derives from an existing tagged parent type, possibly adding new record components and discriminants. The programmer may override existing operations with new implementations. Then, all calls to the existing operation will automatically call the new operation in the appropriate cases; there is no need to change or even to recompile such pre-existing code. For both tagged and non-tagged types, it is possible to write class-wide operations by defining subprograms that take parameters of a class-wide type. Class-wide programming allows the programmer to avoid redundancy in cases where an operation makes sense for all types in a class, and where the implementation of that operation is essentially the same for all types in the class. This is true, for example, of operations that manipulate objects of an integer type. Object-oriented programming is discussed further in section 4.2. 2.3.4. Generic Units Generic program units allow parameterization of program units. The parameters can be types and subprograms as well as objects. A normal (non-generic) program unit is produced by instantiating a generic unit; the normal program unit is said to be an instance of the generic unit. An instance of a generic package is a package; an instance of a generic subprogram is a subprogram. The instance is a copy of the generic unit, with actual parameters substituted for generic formal parameters. Generic units may be implemented by macro substitution (i.e., by actually generating new code for each instance), or by sharing the code for multiple instances, and passing information about the parameters at run time. An example of a generic package is a generic linked list package that works for any element type. The data type of the elements would be passed in as a parameter. The algorithms for manipulating the lists are independent of the actual element type. Instances of the generic package would support linked lists with a particular element type. If the element type is a tagged class-wide type, then heterogeneous lists can be created, containing elements of any type in the class. Generic formal derived types permit generic units to be developed for user-defined classes. Generic formal packages allow a generic unit to be parameterized by another generic package (instance). 2.3.5. Separate Compilation Ada allows the specifications and bodies of program units to be separately compiled. A separately compiled piece of code is called a compilation unit. The Ada compiler provides the same level of compile-time checking across compilation units as it does within a single compilation unit. For example, in a procedure call, the actual parameters must match the types declared for the formal parameters. This rule is checked whether the procedure declaration and the procedure call are in the same compilation unit, or different compilation units. Ada compilers maintain a database called the program library, which contains information about each compilation unit that has been compiled. This information is used in part to check rules across compilation unit boundaries. There are rules about order of compilation that ensure that the compiler always has enough information to check all the rules. For example, a specification must be compiled before all units that have visibility to names declared in that specification. 2.3.6. Library Units A program library contains a collection of compiled library units. The user creates new library units and updates old ones by compiling new Ada source text into the program library. Library units may be packages, subprograms, or generic units. Package library units may have child library units. Thus, an entire hierarchy of library units may be created: the root of the tree is called a root library unit; the tree contains the root library unit, plus all of its children and their descendents. A library unit specification and its body are compilation units; that is, they may be compiled separately. Visibility among library units is achieved using context clauses; a compilation unit can see a particular library unit if it names that library unit in a context clause. Both root library units and child units may be named in a context clause. In addition, the child library units of a parent can see the parent, including the parent's private declarations, and the body of a unit always has visibility into its specification. Child units are discussed in depth in Section 4.3. Child units may be used to reduce recompilation costs: Except for dependences created by context clauses, the immediate children of a given unit may be recompiled in any order. Therefore, if an existing library unit is extended by adding a child unit, the existing unit need not be recompiled; adding a child is accomplished without changing the source code of the parent. More importantly, other units that depend on the existing parent unit will not need to be recompiled. The root library units are like children of package STANDARD: context clauses and compilation ordering rules work the same way. Thus, child units are a straightforward generalization of Ada 83 library units. Example: package ROOT is -- specification of a root library unit ... end ROOT; ------------------------------- package ROOT.CHILD is -- specification of a child library unit ... end ROOT.CHILD; ------------------------------- package body ROOT.CHILD is -- body of the child library unit ... end ROOT.CHILD; ------------------------------- private package ROOT.LOCAL_DEFINITIONS is -- a private child package specification ... end ROOT.LOCAL_DEFINITIONS; ------------------------------- package body ROOT is -- body of the root library unit ... end ROOT; The lines in the above example indicate the separate compilation units; they may be submitted to the compiler separately. Note that the child library units are clearly distinguishable by their expanded names (based on the parent's name). The example also shows a private child package -- a private child unit is visible only within the hierarchy of units rooted at its parent. Sometimes, the body of a library unit becomes very large, because it contains one or more nested bodies. In such cases, Ada allows the nested bodies to be separately compiled as subunits. The nested body is replaced by a body stub. The subunit, which is given separately, must name its parent unit. Visibility within the subunit is as if it had appeared at the place where its body stub occurs. Subunits also support an incremental style of top-down development, because a unit may be compiled with one or more body stubs -- allowing the development of those bodies to be deferred. 2.3.7. Program Composition An executable software system is known in Ada as a program. A program is composed of one or more compilation units. A program may be divided into separate partitions, which represent separate address spaces. Implementations may provide mechanisms for user-defined inter-partition communication. The Distributed Systems Annex defines a minimum, standard interface for such communication. Partitions are intended to support distributed processing, as explained in the Annex. Of course, many programs will not be partitioned; such programs consist of a single partition. To build a partition, the user identifies a set of library units to be included. The partition consists of those library units, plus other library units depended on by the named units. The Ada implementation automatically constructs this set of units before run time. Each partition has an environment task, which is provided automatically by the Ada implementation. A partition may have a main subprogram, which must be a library unit subprogram. The environment task elaborates all of the library units that are part of the partition, and their bodies, in an appropriate order, and then calls the main subprogram, if any. The library units and the main program may create other tasks. Thus, an executing partition contains a dynamic hierarchy of tasks, rooted at the environment task. A program library may contain the library units of one or more programs. Individual library units may be part of more than one program; each program gets its own copy at run time. 2.3.8. Interfacing to Other Languages Large programs are often composed of parts written in several languages. Ada supports this by allowing inter-language subprogram calls, in both directions, and inter-language variable references, in both directions. The user specifies these interfaces using pragmas. 2.4. Multitasking Ada tasking provides a structured approach to concurrent processing under the control of an Ada run-time system, which provides services such as scheduling and synchronization. This section describes tasks and the methods that are used for synchronizing task execution and for communicating between tasks. Tasking is intended to support tightly coupled systems in which the communication mechanisms may be implemented in terms of shared memory. Distributed processing, where the processors are loosely coupled (i.e., they do not share memory), is addressed in the Distributed Systems Annex. 2.4.1. Tasks Tasks are entities whose execution may proceed in parallel. A task has a thread of control. Different tasks proceed independently, except at points where they synchronize. If there is a sufficient number of processors, then all tasks may execute in parallel. Usually, however, there are more tasks than processors; in this case, the tasks will time-share the existing processors, and the execution of multiple tasks will be interleaved on the same processor. 2.4.2. Communication and Synchronization For multiple tasks to cooperate, there must be mechanisms that allow the tasks to communicate and to synchronize their execution. Synchronization and communication usually go hand-in-hand. Ada tasks synchronize and communicate in the following situations: - Tasks synchronize during activation and termination (as explained below). - Protected records provide synchronized access to shared data objects. - Rendezvous are used for synchronous communication between a pair of tasks. - Finally, unprotected access to shared variables is allowed, but requires a disciplined protocol to be enforced by the communicating tasks. This flexibility allows the user to choose the appropriate synchronization/communication mechanisms for the problem at hand. 2.4.2.1. Task Creation, Activation and Termination Tasks are designated by objects of task types. There may be more than one object of a given task type. All objects of a given task type have the same entries (interface), and share the same code (body). As a result they all execute the same algorithm. Different task objects of the same type may be parameterized using discriminants, as for other composite types. Task types are limited types; assignment and equality operations are forbidden. Task objects are created in the same ways as other objects: they may be declared by an object declaration, or created dynamically using an allocator. Tasks may be nested within other program units, in the same manner as subprograms and packages. All tasks created by a given declarative part or allocator are activated in parallel. This means that they can logically start running in parallel with each other. The task that created these tasks waits until they have all finished elaborating their declarative parts; it then continues running in parallel with the tasks it created. Every task has a master, which is the task, subprogram, or block statement which contains the declaration of the task object (or an access type designating the task type, in some circumstances). The task is said to depend on its master. The task executing the master is called the parent task. Before leaving a master, the parent task waits for all dependent tasks. When all of those have been terminated, or are ready to terminate, the parent task proceeds. Tasks may be terminated prematurely with the abort statement. 2.4.2.2. Protected Records Protected record types are limited types; they are used to synchronize access to shared data. A protected record object is like a normal record object, in that it may contain components. However, a protected record may also contain functions, procedures, and entries -- the protected operations of the protected record. The data being shared is declared either as components of the protected record, or as global variables, possibly designated by the protected record components. Calls to the protected operations are synchronized as follows. Protected functions provide shared read-only access to the shared data. Multiple tasks may execute protected functions at the same time. Protected procedures and entries provide exclusive read/write access to the shared data. If any task is executing a protected procedure or entry, then no other tasks are allowed to execute any protected operation at the same time; if they try, they must wait. Protected records are a safe and efficient method of synchronizing shared data access. They are safe, because they perform the necessary synchronization operations automatically, and because all synchronizing operations are collected together syntactically. (This is in contrast to lower-level mechanisms such as semaphores, where the user of the shared data must remember to lock and unlock the semaphore.) They are efficient, because their intended implementation is close to the hardware: spin-locks, for example, in multiprocessor systems. 2.4.2.3. Protected Operations Protected records may export functions, procedures, and entries as described above. Tasks may export entries. All of these protected operations are called using similar syntax: ``OBJ.OP(...)'', where OBJ is the name of the task or protected record object, OP is the name of the protected operation, and ``(...)'' represents any actual parameters. Information is passed back and forth using in, in out, out parameters. Typically, the programmer ensures that operations of protected records execute for a bounded and short period of time. 2.4.2.4. Entries, Queues, Rendezvous, and Barriers A client task which calls an entry of a server task or protected record may be suspended and placed on a queue. When a server task accepts the entry call from a client task, we say that the two tasks are in rendezvous. At the beginning and the end of the rendezvous, data may be exchanged via parameters. When the rendezvous is over, the two tasks each continue execution in parallel. Entries of protected records are controlled by barrier expressions. When a task calls the entry, it can execute the operation immediately if the barrier expression is true; otherwise, the caller is placed on a queue until the barrier has become true. Protected functions and procedures do not have barrier expressions and therefore, need not be queued. 2.4.2.5. Select Statements Select statements are used to specify that a task is willing to wait for any of a number of alternative events. Select statements used by tasks may contain: - One or more entry call alternatives; which indicate that the task is waiting for one of those entries to be executed. That is, the task waits either for another task to accept one of the entries, or for a protected record entry to become open. - An optional abortable final part, which allows a task to provide for an asynchronous transfer of control: If none of the alternatives (entry calls or delay alternatives) is immediately possible, the abortable final part begins execution, but it is aborted if execution of another alternative subsequently becomes possible. Control is asynchronously transferred from somewhere in the abortable final part to the selected alternative. If the abortable final part completes before being aborted, then all of the alternatives are closed, and execution continues at the next statement after the select statement. Select statements used by a server task may contain: - One or more accept alternatives, which indicate that the task is willing to accept one of several entries; that is, the task waits for another task to call one of those entries. - An optional terminate alternative, which allows a server task to specify that it is willing to terminate if there is no more work to do (i.e., all other tasks that depend on the same master are either terminated or are waiting at terminate alternatives). Any select statement can also contain: - A delay alternative (see below), and/or - An else part, which allows a task to specify an action to be taken if communication is not immediately possible. Whichever alternative becomes available first is chosen. Each alternative specifies an action to be executed if and when the alternative is chosen. Table 2-1 shows the major forms of communication and synchronization in Ada. Ada feature Synchronization Communication Task Creation (not needed) Creator initializes d Task Activation Creator waits for Activation failure mi tasks being activated Task Termination Master waits (none) for children Rendezvous Entry caller and Entry parameters are acceptor wait the entry caller and for each other Protected Record mutual exclusion Tasks communicate ind during data access; and writing the compo queued waiting record objects for entry barriers Unprotected user-defined, low-level read/write of shared Shared Variables synchronization Table 2-1: Summary of Communication and Synchronization 2.4.3. Timing Ada provides features for measuring real time. A task may read the clock to find out what time it is. A task may delay for a certain period of time, or until a specific time. A client task may specify a delay alternative, as discussed above. If an entry call is not accepted before the delay expires, then all entry calls are cancelled, and control is transferred to the delay alternative. Similar time-outs are available for server tasks waiting to accept entry calls. 2.4.4. Scheduling Ada separates the concepts of synchronization and scheduling. Synchronization operations determine when tasks must suspend, and when they are ready to run. Scheduling is the method of allocating processors to ready tasks. The default scheduling policy is defined by the language. The Real-Time Systems Annex defines another, priority-based, scheduling policy, based on a dispatching model. Finally, implementations are allowed to add their own policies, which can only be invoked by pragmas. 2.5. Exception Handling Most programs need to recover gracefully from errors that occur during execution. This is especially true of safety-critical systems, which often operate under conditions in which a program ``crash'' would be truly disastrous. Exception handling allows programs to handle such error situations without ceasing to operate. An exception is used to name a particular kind of error situation. Some exceptions are predefined by the language; others may be defined by the user. Exceptions are declared like other entities: BUFFER_FULL_ERROR : exception; This exception might be used to represent the situation of a program trying to insert data into a buffer which is already full. When the exceptional situation happens, the exception is raised. Language-defined exceptions are raised for errors in using predefined features of the language. Language-defined exceptions correspond to run-time errors in other languages. For example, the language-defined exception CONSTRAINT_ERROR is raised when a subtype constraint is violated at run time. User-defined exceptions are raised by the raise statement. To continue the BUFFER_FULL_ERROR example above, the implementation of the buffer data type might contain statements like this: if BUFFER_INDEX > MAX_BUFFER_SIZE then raise BUFFER_FULL_ERROR; end if; Subprograms, package bodies, block statements, task bodies, entry bodies, and accept and select statements may contain exception handlers. An exception handler specifies an action that should be performed when a particular exception is raised. When an exception is raised, the execution of the current sequence of statements is abandoned, and control is transferred to the exception handler, if there is one. Thus, the action of the exception handler replaces the rest of the execution of the sequence of statements that caused the error condition. If there is no exception handler in a particular scope, then the exception is propagated to the calling scope. If the exception is propagated all the way out to the scope of the environment task, then execution of the program is abandoned; this is similar to the way in which program execution is abandoned in other languages when run-time errors are detected. Here is an exception handler: begin ... exception when BUFFER_FULL_ERROR => RESET_BUFFER; when ERROR : others => PRINT("Exception " & SYSTEM.EXCEPTION_NAME(ERROR) & "raised"); PRINT(SYSTEM.EXCEPTION_INFORMATION(ERROR)); end; This handler recognizes two situations: if BUFFER_FULL_ERROR is raised, the buffer is reset. If any other exception is raised, information about that exception is printed. For many applications, it is useful to get such information about an exception when it occurs. A handler may have a choice parameter (ERROR in the example above) of type EXCEPTION_OCCURRENCE. The predefined functions EXCEPTION_NAME and EXCEPTION_INFORMATION each take a parameter of this type and return a STRING, providing the name or additional information about the exception. These facilities are defined in package SYSTEM. 2.6. Input-Output Input-output capabilities are provided in Ada by predefined packages and generic packages: - Sequential files present a logical view of files as sequences of elements. Successive read or write operations to a sequential file result in the transfer of consecutive elements. The generic package SEQUENTIAL_IO may be instantiated for any type to provide operations for creating, opening, closing, deleting, reading and writing sequential files. All elements of a SEQUENTIAL_IO file are of the same type, although if the type is a tagged class-wide type, the elements may have different tags. - Direct files present a logical view of files as indexed sets of elements. The index allows elements to be read or written at any position within a file. The generic package DIRECT_IO provides operations similar to SEQUENTIAL_IO. In addition, DIRECT_IO provides operations for determining the current position and size of a file, and setting the position in the file, in terms of element numbers. - The TEXT_IO package provides facilities for input and output in a human-readable form. Class-wide operations are defined for input and output of values of the integer and real classes to complement the facilities provided by the generic packages INTEGER_IO, FLOAT_IO, and FIXED_IO. - A stream presents a logical view of a file (or other external medium such as a buffer or network channel) as a sequence of storage elements. Stream input and output are predefined as basic operations for all non-limited types. Users may override these default implementations using the facilities of the package STREAM_SUPPORT. 2.7. Low-Level Programming Although the majority of program text can be written in a machine-independent manner, most large software systems contain small portions that need to depend on low-level machine characteristics. Ada allows such dependence, while still allowing the high-level aspects of the algorithms and data structures to be described. Many of an implementation's machine-specific characteristics are accessible through the package SYSTEM. SYSTEM defines storage-related types, an ADDRESS type, and relational and arithmetic operations on addresses. 2.7.1. Pragmas A pragma is used to convey information to the compiler; it is similar to a compiler directive supported by other languages. A pragma begins with the reserved word pragma, an identifier which is the name of the pragma, and optionally one or more arguments. Some pragmas are defined by the language. For example, pragma INLINE indicates to the compiler that the code for a subprogram is to be expanded inline at each call whenever possible. Most pragmas apply to a single object, type, or program unit. Configuration pragmas are used to specify partition-wide or program-wide options. Implementations may provide additional pragmas, as long as they do not syntactically conflict with existing ones or use reserved words. Unrecognized pragmas have no effect on a program, but their presence must be signaled with a warning message. 2.7.2. Specifying Representations Normally, the programmer lets the Ada compiler choose the most efficient way of representing objects. However, Ada also provides representation clauses, which allow the user to specify the representation of an individual object, of all objects of a subtype, or of all objects of a type. Other representation clauses apply to program units. The programmer may need to specify that the representation matches the representation used by some hardware or software external to the Ada program, in order to interface to that external entity. Or, the programmer may wish to specify a more efficient representation of certain objects in cases where the compiler does not have enough information to determine the best (most efficient) representation. In either case, data types, subtypes, and objects are first declared in the normal manner, giving their logical properties. Later in the same declarative part, the programmer gives the representation clauses. In addition to representation clauses, the language defines certain pragmas that control aspects of representation. Implementations may provide additional representation pragmas. There are predefined attributes that allow users to query aspects of representation. These are useful when the programmer needs to write code that depends upon the representation, although the user might not need to control the representation. In the absence of representation clauses or pragmas, the compiler is free to choose any representation. 2.7.3. Unprotected Shared Variables In Ada, variables may be shared among tasks according to the normal visibility rules: if two tasks can see the name of the same variable, then they may use that variable as shared data. However, it is up to the programmer to properly synchronize access to these shared variables. In most cases, data sharing can be achieved more safely with protected records; unprotected shared variables are primarily used in low-level systems programming. Ada allows the user to specify certain aspects of memory allocation and code generation that may affect synchronization by specifying variables as volatile or atomic. 2.7.4. Unchecked Programming Ada provides features for bypassing certain language restrictions. These features are unchecked; it is the programmer's responsibility to make sure that they do not violate the assumptions of the rest of the program. For example, there are mechanisms for manipulating access types that might leave dangling pointers, and there is a mechanism for converting data from one type to another, bypassing the type-checking rules. The generic function UNCHECKED_DEALLOCATION frees storage allocated by an allocator. It is unchecked in the sense that it can leave dangling pointers. The generic function UNCHECKED_CONVERSION converts data from one type to another, bypassing all type-checking rules. The conversion is done simply by reinterpreting the bit pattern as a value of the target type; no conversion actually happens at run time. The attribute P'UNCHECKED_ACCESS returns a typed access value to any aliased object, bypassing the scope checking rules. 2.8. Application-Specific Facilities Previous sections of this Overview have focused on the ``core'' of the Ada language. Implementations may provide additional features, not by extending the language itself, but by providing specialized packages and implementation- defined pragmas and attributes. In order to encourage uniformity among implementations, without restricting functionality, the Specialized Needs Annexes defines standards for such additional functionality for specific application areas. Implementations are not required to support all of these features. For example, an implementation specifically targeted to embedded machines might support the application-specific features for Real-time Systems, but not the application-specific features for Information Systems. The application areas discussed in the Specialized Needs Annexes are: - Systems Programming, including access to machine operations, interrupts, elaboration control, user-defined allocation and finalization, unsigned integers, low-level shared variables, and task identification facilities. - Real-time Systems, including priorities, queuing and scheduling policies, CPU time accounting support, monotonic time, delay accuracy, immediate abort and a simple tasking model. - Distributed Systems, including a model for Ada program distribution into partitions and inter-partition communication. - Information Systems, including support for decimal types, and interfaces to external systems. - Safety and Security, including identification of erroneous executions and bounded error situations. - Numerics, including a model of real arithmetic and the generic elementary and primitive mathematical functions. 2.9. Summary The goal of this chapter is to provide an overview of the language, and to provide the reader with the conceptual framework within which to understand the more detailed discussions that follow. It also demonstrates that the proposed changes to the language represent a natural extension to the original design of Ada. Subsequent chapters demonstrate that the proposed changes also satisfy the requirements, and include examples of the enhanced functionality to show their usefulness. 3. Mapping the Ada 9X Requirements This chapter demonstrates how the proposed Ada 9X mappings satisfy the Ada 9X Requirements [DoD 90]. Wherever possible, examples are used to illustrate the Ada 9X solutions. The presentation here follows that of the requirements document, which distinguishes requirements as either Requirements or Study Topics. The intent of this distinction is that priority be given to satisfying Requirements during the revision. The organization of this chapter approximately parallels the chapters in the requirements document. However, as one goal of the mapping is to satisfy more than one requirement with a single language feature, the presentation cannot strictly follow the order of the requirements. We recommend that the requirements document be kept at hand while reading this chapter as the full text of the requirements is not included herein. 3.1. International Users This section addresses the internationalization of Ada to improve support for non-English character sets. This requirement arose from the ISO standardization of Ada 83, when a commitment was made that improved character set support would be addressed when Ada was revised. Requirement R3.1-A(1) -- Base Character Set Type CHARACTER in STANDARD will have 256 positions rather than only 128 (see MS-3.5.2). Which standard the 8-bit character type follows will depend on the recommendation of the Character Rapporteur Group (CRG) of ISO SC22/WG-9. Currently, the proposed base is ISO 8859. This change introduces a possible upward incompatibility in Ada 83 programs that contain array aggregates or case statements indexed by type CHARACTER. It also effectively doubles the size of arrays indexed by CHARACTER. However, there is a strong consensus in the Ada community that the basic character set should be extended to a full 8-bit set, and so the upward incompatibilities are acceptable given the anticipated user benefit of an 8-bit CHARACTER. Requirement R3.1-A(2) -- Extended Graphics Literals Requirement R3.1-A(3) -- Extended Character Set Support Again, the mapping in this area intends follow the CRG recommendation for extended character support. Ada 9X meets the two Requirements above by introducing types WIDE_CHARACTER and WIDE_STRING to support a 16-bit character set (see MS-3.5.2). Input/output operations for WIDE_STRING and WIDE_CHARACTER will be provided by WIDE_TEXT_IO. Requirement R3.1-A(4) -- Extended Comment Syntax This requirement is already met by Ada 83 (see AI-339). Study Topic S3.1-A(5) -- Extended Identifier Syntax This issue is being discussed by the CRG. Ada 9X intends to follow their recommendations. 3.2. Support for Programming Paradigms This section addresses several topics: subprograms, storage management, composition of program units, generic units, exceptions, and input/output. These have implications for programming-in-the-large and for many types of applications programming. 3.2.1. Subprograms as Data Study Topic S4.1-A(1) -- Subprograms as Objects Requirement R4.1-B(1) -- Passing Subprograms as Parameters Ada 9X provides access-to-subprogram types. A value of such a type may designate any subprogram matching the profile in the type declaration, whose scope does not end before that of the access type. By providing access-to-subprogram types, Ada 9X provides efficient means to - dynamically select and invoke a subprogram with appropriate arguments - store references to subprograms in data structures - parameterize subprograms with other subprograms (at run-time). Access-to-subprogram values are created by the ACCESS attribute. Compile-time scoping rules ensure that a subprogram designated by an access value cannot be called after its enclosing frame has exited. It also allows implementations to create and dereference these access-to-subprogram values very efficiently, since they can be a single address, or an address plus a ``static link''. Example: The program below illustrates the access-to-subprogram features of Ada 9X in the context of event-driven programming. The example implements a button manager, as might be found in the implementation of a windowing system. package PUSH_BUTTONS is type BUTTON is private; -- Two access-to-subprogram types: type BUTTON_ACTION is access procedure; type FEEDBACK_ACTION is access procedure (B : in out BUTTON); function CREATE return BUTTON; procedure PUSH(B : in out BUTTON); procedure SET_ACTION(B : in out BUTTON; A : in BUTTON_ACTION); procedure SET_FEEDBACK(B : in out BUTTON; F : in FEEDBACK_ACTION); procedure HIGHLIGHT(B : in out BUTTON); procedure BEEP; private type BUTTON is record ACTION : BUTTON_ACTION := BEEP'ACCESS; FEEDBACK : FEEDBACK_ACTION := HIGHLIGHT'ACCESS; end record; end PUSH_BUTTONS; Discussion: We have declared two access-to-subprogram types, BUTTON_ACTION and FEEDBACK_ACTION. Values of BUTTON_ACTION may designate any global parameterless procedure (e.g., BEEP). Values of FEEDBACK_ACTION may designate any global procedure with a single in out parameter of type BUTTON (e.g., HIGHLIGHT); this criterion for matching is known as ``subtype conformance'' (see MS-6.3.1). Like values of any other access types, access-to-subprogram values may be stored in data structures. In the example, a BUTTON has an associated BUTTON_ACTION and FEEDBACK_ACTION, which are intended to be invoked when the button is pushed. The default actions for the type are created here by the use of a new attribute P'ACCESS (MS-3.9.2). For a subprogram P, P'ACCESS yields an access value designating that subprogram. Example: Here is the body for the PUSH_BUTTONS package: package body PUSH_BUTTONS is procedure PUSH(B : in out BUTTON) is begin -- Invoking subprograms designated by access values: B.FEEDBACK(B); B.ACTION.all; end PUSH; procedure SET_ACTION (B : in out BUTTON; A : in BUTTON_ACTION) is begin B.ACTION := A; end SET_ACTION; . . . end PUSH_BUTTONS; Discussion: The body of PUSH shows how subprogram ``values'' may be called. On invocation, PUSH provides feedback to the user that something is happening by calling the subprogram designated by the value of B.FEEDBACK. It then performs the action designated by B.ACTION. The .all is necessary because B.ACTION designates a parameterless procedure. The call to B.FEEDBACK does not require a .all, due to the presence of the argument list. Implicitly providing the .all is analogous to access-to-array types. 3.2.2. Subprogram Interoperability Requirement R4.1-B(2) -- Pragma INTERFACE The access-to-subprogram types discussed may be used in conjunction with pragma INTERFACE, and a new pragma, CALLING_CONVENTION (see MS-13.9), to satisfy the requirement for subprogram interoperability. Example: The following example illustrates how Ada 9X procedures may call and be called from a program written in a different language (in this case, ``C''). type XT_CALLBACK is access procedure (WIDGET_ID : in out XT_INTRINSICS.WIDGET; CLOSURE : in X_LIB.X_ADDRESS; CALL_DATA : in X_LIB.X_ADDRESS); pragma CALLING_CONVENTION(C, XT_CALLBACK); procedure XT_ADD_CALLBACK( W : in out XT_INTRINSICS.WIDGET; CALLBACK_NAME : in STRING; CALLBACK : in XT_INTRINSICS.XT_CALLBACK; CLIENT_DATA : in XT_INTRINSICS.XT_POINTER); pragma INTERFACE(C, XT_ADD_CALLBACK, "XtAddCallBack"); procedure MY_CALLBACK( WIDGET_ID : in out XT_INTRINSICS.WIDGET; CLOSURE : in X_LIB.X_ADDRESS; CALL_DATA : in X_LIB.X_ADDRESS) is separate; pragma CALLING_CONVENTION(C, MY_CALLBACK); MY_WIDGET : XT_INTRINSICS.WIDGET; XT_ADD_CALLBACK(MY_WIDGET, "MOUSEDOWN", MY_CALLBACK'ACCESS, XT_INTRINSICS.NULL_DATA); Discussion: The first CALLING_CONVENTION pragma applies to the type XT_CALLBACK, and indicates that values of this type designate subprograms callable from programs written in C. The second CALLING_CONVENTION pragma applies to MY_CALLBACK. This informs the compiler that this procedure is intended to be called from a C program, which may affect how it will reference its parameters. MY_CALLBACK'ACCESS will yield a value compatible with XT_CALLBACK, because the same calling convention is specified for both. The procedure XT_ADD_CALLBACK is implemented as a C subprogram. Its pragma INTERFACE has a third parameter, specifying the external name of the subprogram. This feature provides a standard definition for a capability already found in most Ada 83 compilation systems [Fowler 89]. 3.2.3. Composition of Program Units Study Topic S4.3-A(1) -- Reducing the Need for Recompilation Study Topic S4.3-C(1) -- Enhanced Library Support These two study topics are discussed at length in Section 4.3, which discusses the child library unit concept that is introduced in Ada 9X to solve these issues. These study topics address the needs of very large software systems, and as such are not readily appreciated through small examples. Study Topic S4.3-B(1) -- Programming by Specialization/Extension The Ada 9X solution to this study topic is discussed at length in Section 4.2 of this document. The intent of the requirement is to allow programs to be constructed incrementally through the adaptation of existing units. Examples of how this need has been satisfied in other languages can be found by examining books on Smalltalk-80, Eiffel, or C++. Ada 9X introduces tagged type extension, and class-wide and dispatching operations as a solution to this Study Topic. Example: Below we present a doubly-linked list abstraction, implemented using the tagged types found in Ada 9X. This example shows a general purpose reusable component that can be extended and specialized to meet a large number of needs without changes to the source of this unit, and without recompilation of this unit. package DOUBLY_LINKED is -- Define a doubly-linked list abstraction. type NODE_TYPE is tagged limited private; -- Type of nodes appearing in the list. -- Additional data in nodes to be added via extension. type NODE_PTR is access all NODE_TYPE'CLASS; -- Pointer to NODE_TYPE, or any type derived from it. -- Define add/remove operations, -- assuming head of list is a single NODE_PTR. procedure ADD(ITEM : NODE_PTR; HEAD : in out NODE_PTR); -- Add new node at head of list. procedure REMOVE(ITEM : NODE_PTR; HEAD : in out NODE_PTR); -- Remove node from list, update HEAD if necessary. -- Define insert/extract operations, assuming list -- circularly linked through a header node. procedure INSERT(ITEM : NODE_PTR; AFTER : NODE_PTR); -- Insert new node into circular list after specified node. procedure EXTRACT(ITEM : NODE_PTR); -- Extract designated node from circular list. -- Define functions to iterate forward or backward over list. function NEXT(ITEM : NODE_PTR) return NODE_PTR; function PREV(ITEM : NODE_PTR) return NODE_PTR; private type NODE_TYPE is tagged record PREV : NODE_PTR := null; NEXT : NODE_PTR := null; -- Other components to be added by extension. end record; end DOUBLY_LINKED; Discussion: The example illustrates the specification of a simple, doubly linked list abstraction that may be extended with additional components and operations to create useful heterogeneous linked lists. This example makes use of the following features of Ada 9X: - NODE_PTR is an access type with designated type NODE_TYPE'CLASS. This allows NODE_PTR values to designate objects of type NODE_TYPE, or any derivative of NODE_TYPE. Using this type as the link between nodes allows a single list to contain nodes of different types (i.e. it allows for a heterogeneous list). New types may be added to NODE_TYPE'CLASS without recompiling either the specification or the body of the package DOUBLY_LINKED. (Only homogeneous list structures can be constructed with Ada 83 generic units. Variant records can be used to provide some heterogeneity, but source code changes and possibly extensive recompilation are required to add new variants.) - The operations ADD, REMOVE, INSERT, EXTRACT, NEXT, and PREV are ``class-wide'' operations in that they take parameters of type NODE_PTR, which designates the class-wide type NODE_TYPE'CLASS. Values of NODE_PTR may designate any derivative of NODE_TYPE. Other ``class-wide'' operations are those that take type NODE_TYPE'CLASS directly, rather than an access-to-class-wide type. See MS-3.3 and MS-3.4.1. Example: Now we use the DOUBLY_LINKED abstraction to demonstrate programming by extension. We implement a keyed association abstraction using an extension of DOUBLY_LINKED.NODE_TYPE. The ELEMENT_TYPE is intended to be further extended with the data to be associated with the key. with DOUBLY_LINKED; generic type KEY_TYPE is limited private; with function "="(LEFT, RIGHT : KEY_TYPE) return BOOLEAN is <>; with function HASH(KEY : KEY_TYPE) return INTEGER is <>; package ASSOCIATION is type ELEMENT_TYPE is new DOUBLY_LINKED.NODE_TYPE with record KEY : KEY_TYPE; end record; type ELEMENT_PTR is access all ELEMENT_TYPE'CLASS; type ASSOCIATION_TABLE(SIZE : POSITIVE) is limited private; -- SIZE determines size of hash table. -- It should be a prime number, ideally. procedure ENTER(TABLE : in out ASSOCIATION_TABLE; ELEMENT : in ELEMENT_PTR); function LOOKUP(TABLE : in ASSOCIATION_TABLE; KEY : in KEY_TYPE) return ELEMENT_PTR; -- other operations on ASSOCIATION_TABLE (e.g., an iterator)... private type ASSOCIATION_TABLE(SIZE : POSITIVE) is array(1..SIZE) of ELEMENT_PTR; end ASSOCIATION; Discussion: An ASSOCIATION_TABLE is a hash table, where each hash value has an associated doubly-linked list of elements. The elements may be of any type derived from ELEMENT_TYPE, because the head of each list is of type ELEMENT_PTR, which is an access to ELEMENT_TYPE'CLASS. Example: We now go on to define a symbol table for a simple language with types, objects, and functions using the association structure: with ASSOCIATION; package SYMBOL_TABLE_PKG is type IDENTIFIER is access STRING; -- Symbol table key is pointer to string, -- allowing arbitrary length identifiers. function EQUAL(LEFT, RIGHT : IDENTIFIER) return BOOLEAN; function HASH(KEY : IDENTIFIER) return INTEGER; -- Instantiate ASSOCIATION to produce symbol table package SYMBOL_ASSOCIATION is new ASSOCIATION(IDENTIFIER, EQUAL, HASH); subtype SYMBOL_TABLE is SYMBOL_ASSOCIATION.ASSOCIATION_TABLE; -- Define the three kinds of symbol table elements -- using type extension. type TYPE_SYMBOL is new SYMBOL_ASSOCIATION.ELEMENT_TYPE with record CATEGORY : TYPE_CATEGORY; SIZE : NATURAL; end record; type TYPE_PTR is access TYPE_SYMBOL; type OBJECT_SYMBOL is new SYMBOL_ASSOCIATION.ELEMENT_TYPE with record OBJECT_TYPE : TYPE_PTR; STACK_OFFSET : INTEGER; end record; type FUNCTION_SYMBOL is new SYMBOL_ASSOCIATION.ELEMENT_TYPE with record RETURN_TYPE : TYPE_PTR; FORMALS : SYMBOL_TABLE(5); -- very small hash table LOCALS : SYMBOL_TABLE(19); -- bigger hash table FUNCTION_BODY : STATEMENT_LIST; end record; end SYMBOL_TABLE_PKG; Discussion: A symbol table is produced by instantiating the generic ASSOCIATION with a key that is a pointer to a string. Then three extensions of the symbol table ELEMENT_TYPE are declared, each of which may be entered into the symbol table. Example: Here is a partial implementation of the body of the generic ASSOCIATION package: package body ASSOCIATION is procedure ENTER(TABLE : in out ASSOCIATION_TABLE; ELEMENT : ELEMENT_PTR) is -- Enter new element into association table. HASH_INDEX : constant INTEGER := (HASH(ELEMENT.KEY) mod TABLE'LENGTH) + 1; use DOUBLY_LINKED; begin -- Add to linked list of appropriate bucket. ADD(NODE_PTR(ELEMENT), NODE_PTR(TABLE(HASH_INDEX))); end ENTER; function LOOKUP(TABLE : ASSOCIATION_TABLE; KEY : KEY_TYPE) return ELEMENT_PTR is -- Look up element in association table. HASH_INDEX : constant INTEGER := (HASH(KEY) mod TABLE'LENGTH) + 1; PTR : ELEMENT_PTR := TABLE(HASH_INDEX); -- Head of list. use DOUBLY_LINKED; begin -- Scan doubly-linked list for element with -- matching key. Return null if none found. while PTR /= null loop if PTR.KEY = KEY then -- Found matching element; return it. return PTR; end if; PTR := ELEMENT_PTR(NEXT(NODE_PTR(PTR))); end loop; return null; -- No matching element found. end LOOKUP; . . . end ASSOCIATION; Discussion: The operations ENTER and LOOKUP are implemented in a straightforward manner using the operations of the access type NODE_PTR. Access type conversion is used to allow objects of type ELEMENT_PTR to use the operations of type NODE_PTR (see MS-4.6 for Ada 9X rules on access type conversion). 3.2.4. Storage Management Requirement R4.2-A(1) -- Allocation and Reclamation of Storage Ada 9X enables users to write their own heap management procedures for specified storage pools, thus providing a straightforward solution to the requirement (see MS-G, Systems Programming Annex). Example: This example shows how an application can use a special allocator to meet its precise storage requirements. The storage pool associated with the access type supports mark and release operations, allowing rapid reclamation of recently allocated storage when a task continues after a failure. with SYSTEM; package MARK_RELEASE_STORAGE is type MARK_RELEASE_POOL ( SIZE : SYSTEM.STORAGE_COUNT; BLOCK_SIZE : SYSTEM.STORAGE_COUNT) is new SYSTEM.ROOT_STORAGE_POOL with limited private; type POOL_MARK is limited private; -- primitive operations of SYSTEM.ROOT_STORAGE_POOL -- overridden for the MARK_RELEASE_POOL procedure INITIALIZE( POOL : in out MARK_RELEASE_POOL); procedure FINALIZE(POOL : in out MARK_RELEASE_POOL); procedure ALLOCATE( POOL : in out MARK_RELEASE_POOL; ADDRESS : out SYSTEM.ADDRESS; STORAGE_SIZE : in SYSTEM.STORAGE_COUNT; ALIGNMENT : in SYSTEM.STORAGE_COUNT); procedure DEALLOCATE( POOL : in out MARK_RELEASE_POOL; ADDRESS : in SYSTEM.ADDRESS; STORAGE_SIZE : in SYSTEM.STORAGE_COUNT); -- additional subprograms for the MARK_RELEASE_POOL procedure SET_MARK( POOL : in MARK_RELEASE_POOL; MARK : out POOL_MARK); -- Marks the current state of the pool for later release procedure RELEASE_TO_MARK( POOL : in out MARK_RELEASE_POOL; MARK : in POOL_MARK); -- Frees everything allocated from the POOL since SET_MARK. -- All access values designating objects allocated since then -- become invalid. private . . . end MARK_RELEASE_STORAGE; Discussion: This example demonstrates how a package defines a special mark/release storage pool, derived from SYSTEM.ROOT_STORAGE_POOL, whose ALLOCATE and DEALLOCATE procedures are invoked implicitly by the Ada 9X new and UNCHECKED_DEALLOCATION facilities. This example includes two additional operations on the storage pool type, which the user can use to set a mark and then later release the pool to a marked state, and thereby reclaim all recently allocated storage. The declaration of MARK_RELEASE_POOL indicates that it is also extended with additional limited private components that would be supplied in the private part. Example: Use of a mark/release pool: use MARK_RELEASE_STORAGE; POOL : MARK_RELEASE_POOL(5000, 100); type SOME_TYPE is . . .; type SOME_ACCESS is access SOME_TYPE; for SOME_ACCESS'STORAGE_POOL use POOL; declare MARK : POOL_MARK; DONE : BOOLEAN := FALSE; begin -- Set mark prior to commencing the loop SET_MARK(POOL, MARK); while not DONE loop -- Each iteration allocates a data structure composed of -- SOME_TYPE objects, which may be discarded -- before the next iteration. declare X, Y : SOME_ACCESS; begin -- Algorithm that allocates various objects from -- the mark/release storage pool. Each allocator becomes -- a call on MARK_RELEASE_POOL'ALLOCATE. X := new SOME_TYPE; . . . Y := new SOME_TYPE; . . . -- Release storage each time through the loop RELEASE_TO_MARK(POOL, MARK); exception when others => -- release storage then reraise the exception RELEASE_TO_MARK(POOL, MARK); raise; end; end loop; end; Discussion: This example shows how the MARK_RELEASE_POOL might be used in a program. POOL, a MARK_RELEASE_POOL object, is associated with the access type via the representation clause. POOL's first discriminant of 5000 acts as an initialization parameter. It might indicate, for example, the total size of the POOL, or the size of an initial statically allocated chunk, or a count of the number of objects expected to be allocated from that pool. 3.2.4.1. Finalization Study Topic S4.2-A(2) -- Preservation of Abstraction To preserve abstraction, while providing automatic reclamation of resources, Ada 9X provides controlled types that have initialization and finalization operations associated with them (see MS-G, Systems Programming Annex). A type is controlled if it is derived from the language-defined type CONTROLLED in package FINALIZATION_SUPPORT. This type can be extended with the components that require initialization and finalization. User-defined initialization and finalization can be implemented by overriding the default implementations of INITIALIZE and FINALIZE for the derived type. These capabilities allow the implementor of an abstraction to ensure that proper cleanup is performed prior to the object becoming permanently inaccessible. The ability to associate automatic finalization actions with an abstraction is extremely important for Ada, given the orientation toward information hiding, coupled with the many ways that a scope may be exited in Ada (exception, exit, return, abort, asynchronous transfer of control (MS-9.9.3), etc.). In many cases, the need for finalization is more of a safety or reliability issue than a part of the visible specification of an abstraction. Most users of an abstraction should not need to know whether the abstraction uses finalization. Example: A limited type with user-defined finalization: with FINALIZATION_SUPPORT; generic type INDEX_TYPE is (<>); -- Index type must be some discrete type type ELEMENT_TYPE is private; -- Component type for extensible array EMPTY_VALUE : in ELEMENT_TYPE; -- Value to return for never-assigned components package EXTENSIBLE_ARRAY_PKG is -- This generic package defines an extensible array type. type EXTENSIBLE_ARRAY_TYPE is new FINALIZATION_SUPPORT.CONTROLLED with limited private; procedure SET_ELEMENT(ARR : in out EXTENSIBLE_ARRAY_TYPE; INDEX : in INDEX_TYPE; VALUE : in ELEMENT_TYPE); -- Set value of an element -- Extend array as necessary function ELEMENT(ARR : EXTENSIBLE_ARRAY_TYPE; INDEX : INDEX_TYPE) return ELEMENT_TYPE; -- Return element of array. -- Return EMPTY_VALUE if never assigned. procedure FINALIZE(ARR : in out EXTENSIBLE_ARRAY_TYPE); -- Reset array to be completely empty. -- Use default INITIALIZE implementation (i.e., no action) private -- Implement using a B-tree-like representation. type ARRAY_CHUNK; -- type completed in package body. type ARRAY_CHUNK_PTR is access ARRAY_CHUNK; type EXTENSIBLE_ARRAY_TYPE is new FINALIZATION_SUPPORT.CONTROLLED with record NUMBER_OF_LEVELS : NATURAL := 0; ROOT_CHUNK : ARRAY_CHUNK_PTR; end record; end EXTENSIBLE_ARRAY_PKG; Discussion: This generic package defines an extensible array type. The array is extended automatically as new components are assigned into it. EMPTY_VALUE is returned on dereferencing a never-assigned component. On scope exit, automatic finalization calls FINALIZE, which reclaims any storage allocated to the extensible array. Example: The body for the extensible array package: package body EXTENSIBLE_ARRAY_PKG is type ARRAY_CHUNK is . . . -- Complete the incomplete type definition. procedure SET_ELEMENT( . . . function ELEMENT(ARR : EXTENSIBLE_ARRAY_TYPE; INDEX : INDEX_TYPE) return ELEMENT_TYPE is begin if ARR.ROOT_CHUNK = null then -- Entire array is empty. return EMPTY_VALUE; else -- Look up to see if INDEX appears -- in array somewhere. . . . end if; end ELEMENT; procedure FINALIZE(ARR : in out EXTENSIBLE_ARRAY_TYPE) is begin if ARR.ROOT_CHUNK /= null then -- Release all chunks of array . . . -- Reinitialize array back to initial state. ARR.ROOT_CHUNK := null; ARR.NUMBER_OF_LEVELS := 0; end if; end FINALIZE; end EXTENSIBLE_ARRAY_PKG; Discussion: Since the extensible array type is derived from a library level tagged type (FINALIZATION_SUPPORT.CONTROLLED), the generic unit must also be instantiated at the library level (see MS-3.4.1 and MS-12.1). However, an object of the extensible array type defined in the instantiation may be declared in any scope. When that scope is exited, for whatever reason, the storage dynamically allocated to the array will be reclaimed, via an implicit call on EXTENSIBLE_ARRAY_TYPE.FINALIZE. Such an extensible array type may safely be used by subprograms of a long-running program, without concern for progressive loss of storage. When such subprograms return, the storage will always be reclaimed, whether exited by an exception, return, abort, or asynchronous transfer of control. We considered a general user-defined assignment and finalization capability. However, because of the large number of places within the language where implicit assignment or copy semantics are used (e.g., function return, aggregates, initialized allocators, etc.), user-defined assignment was determined to be too great an implementation burden if required of all Ada compilers. 3.2.5. Generics Study Topic S4.4-A(1) -- Generic Formal Parameters Ada 9X extends the flexibility of generics by defining two new kinds of generic formal parameter (see MS-12.1). Generic formal package instantiations allow generics to be parameterized by other generics, which allows for safer and simpler composition of generic abstractions. In particular it allows for one generic to easily extend the abstraction provided by another generic, without requiring the programmer to enumerate all the operations of the first in the formal part of the second. Example: Below we present a simple generic package that implements complex numbers. It is then extended with a generic matrix package that works with instances of the generic complex numbers. generic type FLOAT_TYPE is digits <>; package GENERIC_COMPLEX_FUNCTIONS is type COMPLEX is record REAL : FLOAT_TYPE; IMAG : FLOAT_TYPE; end record; function "-"(RIGHT : COMPLEX) return COMPLEX; function "+"(LEFT, RIGHT : COMPLEX) return COMPLEX; . . . end GENERIC_COMPLEX_FUNCTIONS; with GENERIC_COMPLEX_FUNCTIONS; generic with package COMPLEX_FUNCTIONS is new GENERIC_COMPLEX_FUNCTIONS(<>); package GENERIC_COMPLEX_MATRIX_OPERATIONS is type COMPLEX_MATRIX is array(POSITIVE range <>, POSITIVE range <>) of COMPLEX_FUNCTIONS.COMPLEX; function "*"(LEFT : COMPLEX_FUNCTIONS.COMPLEX; RIGHT : COMPLEX_MATRIX) return COMPLEX_MATRIX; . . . end GENERIC_COMPLEX_MATRIX_OPERATIONS; -- A pair of instantiations: package SHORT_COMPLEX_PKG is new GENERIC_COMPLEX_FUNCTIONS(SHORT_FLOAT); package SHORT_COMPLEX_MATRIX_PKG is new GENERIC_COMPLEX_MATRIX_OPERATIONS(SHORT_COMPLEX_PKG); Discussion: GENERIC_COMPLEX_FUNCTIONS is parameterized by any floating point type. It provides simple arithmetic operations on complex numbers. GENERIC_COMPLEX_MATRIX_OPERATIONS provides unconstrained matrices with complex coefficients. It is parameterized by an instantiation of the previous generic package. Within the second package, all operations provided by the first generic are available. The last two packages illustrate how these generics are instantiated. Generic formal derived types permit generic units to be parameterized by a user-defined class -- the set of all types derived from the parent type specified in the generic parameter declaration. Within the generic template, the operations of the specified parent type are available. This provides support for user-defined classes that is comparable to that available for predefined classes, such as discrete, integer, private, fixed and float. A generic formal derived type may be used to parameterize a generic with a record type, and together with formal package instantiations, addresses several revision requests relating to generics (RR-505, RR-627, RR-722, AI-452). Example: Below we present a generic package for providing I/O for types in a user-defined rational class. package RATIONAL_ARITHMETIC is -- this package defines a rational number type type RATIONAL is limited private; procedure ASSIGN ( LEFT : out RATIONAL; RIGHT : in RATIONAL); function "+"(LEFT, RIGHT : RATIONAL) return RATIONAL; ... end RATIONAL_ARITHMETIC; with RATIONAL_ARITHMETIC; use RATIONAL_ARITHMETIC; with TEXT_IO; generic -- this package provides I/O for any derivative of the RATIONAL typ type NUM is new RATIONAL; package RATIONAL_IO is procedure GET( FILE : in TEXT_IO.FILE_TYPE; ITEM : out NUM; WIDTH : in TEXT_IO.FIELD := 0); procedure PUT( FILE : in TEXT_IO.FILE_TYPE; ITEM : in NUM; FORE : in TEXT_IO.FIELD; AFT : in TEXT_IO.FIELD; EXP : in TEXT_IO.FIELD); end RATIONAL_IO; Discussion: The generic formal parameter NUM will only match RATIONAL and its derivatives. Since RATIONAL and its derivatives all share the primitive operations of the RATIONAL type, those operations are available within RATIONAL_IO for implementing the GET and PUT subprograms. Requirement R4.4-B(1) -- Dependence of Instantiations on Bodies Some Ada 83 implementations will recompile a package containing a generic instantiation when the body of the generic is recompiled. During the revision phase, the rules of RM 10.3 will be modified to require that only the code corresponding to the body of an instantiation is to be recompiled, and that the unit containing the instantiation should not become out-of-date. Study Topic S4.4-B(2) -- Tighten the ``Contract Model'' A tight contract model has several desirable properties. It allows implementations to share generics more easily, it leads to the early detection of programming errors, and it eliminates the need to recheck all existing instantiations when a new body is compiled. Ada 9X strengthens the contract model by requiring that the specification of a generic formal private type indicate whether a corresponding actual may be an unconstrained composite subtype. This simplifies the checking required when a new generic body is compiled, since its legality will not depend on the nature of the existing instantiations. Example: This example illustrates unconstrained and constrained generic formal private types: generic type KEY(<>) is private; type ITEM is private; package KEYED_INDEX is ... end; Discussion: A generic formal private type declaration with ``(<>)'' for the discriminant part allows the actual to be an unconstrained composite subtype. Without the ``(<>)'', the actual type must be constrained, if composite, or have defaults for all discriminants. The type STRING, because it is an unconstrained array type, could be associated with KEY, but not with ITEM. Within the generic, KEY must not be used to declare a component or uninitialized object. This change is not strictly upward compatible, but a simple work around exists. If existing instantiations fail to compile under Ada 9X, then the generic unit must be modified to specify that the relevant generic formal allows unconstrained types. As the Requirements Document points out in the discussion of Study Topic S4.4-B(2) Tighten the ``Contract Model'', both tight and loose contract models are desirable, each for its own reasons. This tension has been resolved in Ada 9X, by specifying that certain checks in the generic specification are only performed at instantiation time (see MS-12.1). Within the body of the generic, all checks are performed when the generic body is compiled, and these checks fail if any possible instantiation could fail the checks. Through this differentiation, Ada 9X achieves most of the goals associated with a tight contract model, and still provides the flexibility required to use generics to their best advantage. Requirement R4.4-C(1) -- Generic Code Sharing The generic sharing pragma suggested by the requirement is not provided by Ada 9X. However, the user need is largely met by providing class-wide operations (see MS-3.3.3), and by allowing a generic to be instantiated with a class-wide type. Example: For instance, to produce an instantiation of INTEGER_IO that is usable for all integer types, it can be instantiated with the class-wide type for the integer class, namable as STANDARD.INT_CLASS. package ANY_INTEGER_IO is new TEXT_IO.INTEGER_IO(INT_CLASS); Discussion: The PUT and GET subprograms in ANY_INTEGER_IO may be used with objects or values of any integer type. The actual code will be generated by using the longest supported integer type (with range SYSTEM.MIN_INT .. SYSTEM.MAX_INT), consistent with the Ada 83 rules for manipulating universal integer values at run-time (see RM 4.10(5)). In addition to tightening the contract model, we have also proposed to relax the requirement that exceptions declared in a generic body be replicated in each instantiation (see MS-12.2). Together, these changes should simplify the implementation of code sharing between generic instantiations. 3.2.6. Exceptions Requirement R4.5-A(1) -- Accessing an Exception Name Ada 9X allows an exception handler to declare a choice parameter to represent the handled exception. A choice parameter is a constant of type SYSTEM.EXCEPTION_OCCURRENCE. Package SYSTEM also contains the definition of two functions that take an exception occurrence as a parameter. One returns the exception name, the other returns a string that contains implementation defined information about the exception. For example, the location of the exception could be returned. Example: Use of an exception choice parameter: begin . . . -- Algorithm that might raise any number of exceptions exception when CE : CONSTRAINT_ERROR => TEXT_IO.PUT_LINE("CONSTRAINT_ERROR raised:"); -- also print out additional information TEXT_IO.PUT_LINE(SYSTEM.EXCEPTION_INFORMATION(CE)); -- handle CONSTRAINT_ERROR . . . when E : others => -- Print the name of the exception TEXT_IO.PUT_LINE(SYSTEM.EXCEPTION_NAME(E) & " raised."); raise; end; Discussion: The preceding code demonstrates how the name of the active exception can be printed. Within the CONSTRAINT_ERROR handler the variable CE is bound to a value of type EXCEPTION_OCCURRENCE. Messages containing information about the exception, including the information returned by the function EXCEPTION_INFORMATION, are printed out prior to handling the exception. The others handler contains an example of just printing out the exception name. 3.2.7. Input/Output Requirement R4.6-A(1) -- Interactive TEXT_IO We have not addressed the requirement for adding standard interactive input/output support to TEXT_IO. However, the Ada 9X capability to add child library units to TEXT_IO should allow additional operations to be provided by an implementation without changing the TEXT_IO package specification. Requirement R4.6-B(1) -- Additional Input/Output Functions An APPEND_FILE FILE_MODE has been added to the standard file modes for SEQUENTIAL_IO and TEXT_IO in Ada 9X. A general stream-oriented I/O package has been provided to support heterogeneous I/O. Example: The following example shows how different records could be written out to the same file. with STREAM_SUPPORT.STREAM_IO; package body DATA_SUPPORT is . . . type HEADER is record COUNT : POSITIVE; -- other header information end record; type DATA is record -- data fields end record; type DATA_ARRAY is array (POSITIVE range <>) of DATA; . . . procedure DUMP_DATA ( FILE_NAME : in STRING; PRIMARY : in DATA; SECONDARY : in DATA_ARRAY) is use STREAM_SUPPORT; HDR : HEADER; DATA_FILE : STREAM_IO.FILE_TYPE; DATA_STREAM : STREAM_ACCESS; begin STREAM_IO.OPEN(DATA_FILE, STREAM_IO.APPEND_FILE, FILE_NAME); DATA_STREAM := STREAM_IO.STREAM(DATA_FILE); HDR.COUNT := SECONDARY'LENGTH + 1; HEADER'WRITE(DATA_STREAM, HDR); DATA'WRITE(DATA_STREAM, PRIMARY); for I in SECONDARY'RANGE loop DATA'WRITE(DATA_STREAM, SECONDARY(I)); end loop; STREAM_IO.CLOSE(DATA_FILE); end DUMP_DATA; end DATA_SUPPORT; 3.3. Real-Time Requirements The mapping proposals discussed in this section address the Ada 9X requirements for real-time systems. Section 3.3.1 discusses time management issues. Section 3.3.2 discusses issues related to task scheduling, and presents Ada 9X methods for programming common real-time paradigms, including critical regions and shared data. Section 3.3.3 discusses asynchronous transfer of control. Finally, section 3.3.4 discusses asynchronous communication. 3.3.1. Time Requirement R5.1-A(1) -- Elapsed Time Measurement In Ada 83, the type CALENDAR.TIME represents a point in time, and time intervals are measured by the type STANDARD.DURATION. Ada 9X generalizes these notions by providing two time types that are used to address the time management requirements. Ada 9X provides the package MONOTONIC which, like CALENDAR, contains a type TIME and a CLOCK function. MONOTONIC.CLOCK returns values that are guaranteed to be non-decreasing, whereas CALENDAR.CLOCK might be readjusted to match an external wall clock. (See MS-H, Real-Time Systems Annex.) Requirement R5.1-B(1) -- Precise Periodic Execution Ada 9X introduces the delay until statement, which causes a task to delay until a specified time, rather than for a specified time interval. A delay until statement may be parameterized by either CALENDAR.TIME or MONOTONIC.TIME. Delay until statements may be used in select alternatives in the same manner as Ada 83 delay statements. Example: Below we present a task that awakens each night at midnight and performs some logging function. task body AT_MIDNIGHT is ONE_DAY : constant CALENDAR.DAY_DURATION := 86_400.0; NOW : CALENDAR.TIME := CALENDAR.CLOCK; MIDNIGHT : CALENDAR.TIME := CALENDAR.TIME_OF( YEAR => CALENDAR.YEAR(NOW), MONTH => CALENDAR.MONTH(NOW), DAY => CALENDAR.DAY(NOW), SECONDS => 0.0); -- Truncate current time to most recent midnight begin loop MIDNIGHT := MIDNIGHT + ONE_DAY; delay until MIDNIGHT; LOG_DATA; end loop; end AT_MIDNIGHT; Discussion: Since the delay until expression specifies a time rather than a time interval, there is no opportunity for preemption during the calculation of the interval, and therefore the delay will expire at precisely the specified time. Similarly, since the delay is written in terms of the time-of-day clock in the package CALENDAR, if the time-of-day clock is changed to daylight savings time, the delay expiration time will be relative to the new setting of the clock. Example: Below we present a task that polls a device every 10 milliseconds. task body POLL_DEVICE is POLL_TIME : MONOTONIC.TIME := time to start polling; PERIOD : constant MONOTONIC.FINE_DURATION := 0.010; -- 10 milliseconds begin loop delay until POLL_TIME; ... -- Poll the device POLL_TIME := POLL_TIME + PERIOD; end loop; end POLL_DEVICE; Discussion: The POLL_DEVICE task polls the device every 10 milliseconds starting at the initial value of POLL_TIME. The period will not drift, as explained above for the AT_MIDNIGHT example. We use MONOTONIC.TIME instead of CALENDAR.TIME in this example, because we do not wish to be sensitive to possible changes to the time-of-day clock. Requirement R5.1-C(1) -- Detection of Missed Deadlines The solution to this requirement will be discussed in Section 3.3.3 along with other issues relevant to asynchronous transfer of control. 3.3.2. Task Scheduling and Real-Time Paradigms Requirement R5.2-A(1) -- Alternative Scheduling Algorithms MS-H, Real-Time Systems Annex specifies alternate scheduling policies, the ability to modify priorities dynamically, and a priority model. The priority model encompasses many aspects of scheduling, including the selection of alternatives in selective waits, the selection of entries from entry queues, and the priorities at which interrupts are handled. Also, implementations are allowed to define additional implementation-defined scheduling policies. The ability to dynamically modify task priorities, and the ability to choose between the two scheduling policies, or to use another implementation-defined one, give Ada 9X programmers a great deal of freedom in defining the scheduling behavior of a program. Requirement R5.2-A(2) -- Common Real Time Paradigms This requirement lists candidate paradigms whose implementation should be efficient: - Resetting task priorities in response to mode changes; - Controlling access to shared data; - Critical Regions; and - Asynchronous Communication. Ada 9X meets these requirements by defining low level building blocks, with which users can solve each problem to suit an application's specific needs. The abstraction mechanisms of Ada can be used to parameterize these solutions and make them reusable. The real-time building blocks of Ada 9X are: - The Priority Model (MS-H, Real-Time Systems Annex); - Protected Records (MS-9.5); - Selective Entry Calls (MS-9.9); - Asynchronous Transfer of Control (MS-9.9.3). Dynamic priorities can be used to provide scheduling control to the user, perhaps to respond to a system mode change (see 3.3.2.2). If the tasks must also be redirected to perform different work, then asynchronous transfer of control may also be used to effect the mode change. Protected records can be used to define critical regions or to access shared data in a manner guaranteed to be portable and efficient. Protected records and selective entry calls can be used to program various asynchronous communication facilities. In the following example we demonstrate how the Ada 9X building blocks can be used to define a mailbox abstraction. In this example, conditional critical regions and efficient access to shared data, both provided by a protected record, are used to construct the abstraction. In section 3.3.4, this mailbox abstraction, along with the selective entry call, is used to construct a database server that uses asynchronous communication. 3.3.2.1. Example of Conditional Critical Regions and Shared Data Below is an example of a bounded buffer, or ``mailbox,'' implemented with a protected record. In this example a protected record provides conditional critical regions, which allow the abstraction to be used safely by multiple tasks. generic type ITEM is private; MBOX_SIZE : in NATURAL; package MAILBOX_PKG is type ITEM_COUNT is range 0..MBOX_SIZE; type ITEM_INDEX is range 0..MBOX_SIZE-1; type ITEM_ARRAY is array(ITEM_INDEX) of ITEM; protected type MAILBOX is -- Put a data element into the buffer: entry SEND(ELEM: ITEM); -- Retrieve a data element from the buffer: entry RECEIVE(ELEM: out ITEM); private record COUNT : ITEM_COUNT := 0; OUT_INDEX : ITEM_INDEX := 0; IN_INDEX : ITEM_INDEX := 0; DATA : ITEM_ARRAY; end record; end MAILBOX_PKG; Discussion: This example illustrates a generic mailbox abstraction. The protected record has two entries, which insert and retrieve items to and from the mailbox buffer. Like a private type, the data components of the protected record are of no concern outside the body of the protected record. They are declared in the specification so that a compiler can statically allocate all the space required for an instance of the protected record type. Example: The body of the mailbox package: package body MAILBOX_PKG is protected body MAILBOX is entry SEND(ELEM: ITEM) when COUNT < DATA'LENGTH is -- suspend until there's room in the mailbox begin DATA(IN_INDEX) := ELEM; IN_INDEX := (IN_INDEX + 1) mod DATA'LENGTH; COUNT := COUNT + 1; end SEND; entry RECEIVE(ELEM: out ITEM) when COUNT > 0 is -- suspend until there is something in the mailbox begin ELEM := DATA(OUT_INDEX); OUT_INDEX := (OUT_INDEX + 1) mod DATA'LENGTH; COUNT := COUNT - 1; end RECEIVE; end MAILBOX; end MAILBOX_PKG; Discussion: SEND waits until there is room for a new ITEM in the mailbox buffer. RECEIVE waits until there is at least one ITEM in the buffer. The semantics of protected records guarantee that multiple tasks cannot modify the contents of the mailbox simultaneously. 3.3.2.2. Task IDs, Dynamic Priorities, and Mode Change Ada 9X defines a class-wide type for the task class, TASK_CLASS. This type is used by the DYNAMIC_PRIORITY_SUPPORT package (see MS-H, Real-Time Systems Annex) to allow the priority of a task of any type to be altered dynamically. In addition, an access type designating TASK_CLASS may be used as a kind of ``task ID,'' so that arrays or lists of tasks of various types can be managed by a scheduling task. An access-to-class-wide task value, usable as a task ID, is produced by taking the ACCESS attribute of a task object (see MS-3.9.2, MS-9.2). The scope checking rules associated with ACCESS (MS-3.9.2, MS-3.9) ensure that such a task ID for a nested task object is not used outside the scope of the task object. Example: Given this support for a kind of task ID, plus the DYNAMIC_PRIORITY_SUPPORT package, a centralized scheduling task can be established that waits for a mode change event, and then resets the priorities of the tasks under its management: with DYNAMIC_PRIORITY_SUPPORT; use DYNAMIC_PRIORITY_SUPPORT; package body MODE_CHANGE_MANAGEMENT is -- Package to handle mode change by adjusting priorities -- of tasks being managed. type SYSTEM_MODE is (NORMAL, ...); type TASK_ACCESS is access all SYSTEM.TASK_CLASS; type TASK_LIST is array (INTEGER range <>) of TASK_ACCESS; type PRIORITY_LIST is array (INTEGER range <>) of SYSTEM.ANY_PRIORITY; NUM_MANAGED : constant := number of tasks managed; TASKS_BEING_MANAGED : TASK_LIST(1..NUM_MANAGED) := (T1'ACCESS, T2'ACCESS, T3'ACCESS, ...); -- assume T1, T2, etc. are tasks declared elsewhere PRIORITY_IN_MODE : array (SYSTEM_MODE) of PRIORITY_LIST(1..NUM_MANAGED) := (NORMAL => (T1_NORMAL, T2_NORMAL, T3_NORMAL, ...), MODE2 => (T1_MODE2, T2_MODE2, T3_MODE2, ...), MODE3 => ...); -- Priority for each managed task, in each mode. protected MODE_CONTROL is procedure SET(NEW_MODE : SYSTEM_MODE); pragma PRIORITY(SYSTEM.PRIORITY'LAST); end MODE_CONTROL; protected body MODE_CONTROL is procedure SET(NEW_MODE : SYSTEM_MODE) is begin for I in 1 .. NUM_MANAGED loop; SET_PRIORITY(TASKS_BEING_MANAGED(I).all, PRIORITY_IN_MODE(NEW_MODE)(I)); end loop; end SET; end MODE_CONTROL; task body MANAGER_TASK is MANAGER_MODE : SYSTEM_MODE := NORMAL; NEW_MODE : SYSTEM_MODE; begin -- Loop waiting for mode change, and then -- reset priorities accordingly. loop MODE_MANAGER.WAIT_FOR_MODE_CHANGE (MANAGER_MODE)(NEW_MODE); -- Suspend until mode changes. -- (See 4.4.3.2) -- We are in a new mode, reset priorities. MODE_CONTROL.SET(NEW_MODE); -- Reset manager's mode, and loop around. MANAGER_MODE := NEW_MODE; end loop; end MANAGER_TASK; end MODE_CHANGE_MANAGEMENT; Discussion: This example illustrates the use of the ACCESS attribute of a task as its task ID, allowing an array of tasks to be managed. Assume MODE_MANAGER is a protected record containing an entry family that allows a caller to suspend waiting for a mode change (see 4.4.3.2). The family index is the current mode of the caller, and an out parameter returns the new mode when the system mode is not the same as the caller's. The MANAGER_TASK loops, calling the entry WAIT_FOR_MODE_CHANGE, which suspends the caller until the mode has changed from that identified by MANAGER_MODE, and then returns with the new mode stored in NEW_MODE. The new mode returned from WAIT_FOR_MODE_CHANGE is passed as a parameter to the protected procedure SET. This procedure loops through all the tasks being managed resetting the priorities. For each call to SET_PRIORITY in SET, the task is identified using the task ID in the TASKS_BEING_MANAGED array, and the new mode is determined by using the new mode as an index into the array of arrays PRIORITY_IN_MODE. The MANAGER_TASK then loops around waiting for another mode change. 3.3.3. Asynchronous Control of Execution Requirement R5.3-A(1) -- Asynchronous Transfer of Control Requirement R5.1-C(1) -- Detection of Missed Deadlines Both of these requirements are satisfied by the ability to specify a selective entry call statement with an abortable final part (see MS-9.9.2 and MS-9.9.3). If one of the select alternatives is not immediately selected, then the abortable final part starts executing. If one of the select alternatives is selected before the abortable final part completes then the abortable final part is aborted, and the sequence of statements of the selected alternative is executed. Example: Below we demonstrate how missed deadlines may be detected and handled in Ada 9X using the asynchronous transfer of control mechanism. The example is based on an automated teller machine. . . . begin READ_CARD(CARD_DATA); select delay ACCEPTABLE_VALIDATION_WAIT; raise CARD_VALIDATION_TIMEOUT; or KEYBOARD.CANCEL_PRESSED; raise TRANSACTION_CANCELED; then abort VALIDATE_CARD(CARD_DATA); end; PERFORM_TRANSACTION(CARD_DATA); exception when TRANSACTION_CANCELED => . . . when CARD_VALIDATION_TIMEOUT | COMMUNICATION_ERROR => DISPLAY_APOLOGIES; RETURN_CARD; -- Handler higher up tries to restart communications raise; end; Discussion: The procedure VALIDATE_CARD communicates with a central site to determine that the card is valid. This communication is external to the Ada program. If the card cannot be validated in a specified period of time, then the transaction is aborted, and the card is returned. KEYBOARD is a protected record. The barrier for KEYBOARD.CANCEL_PRESSED becomes true when the cancel key is pressed. This causes the entry to be selected, and the validation routine to be abandoned. The implementation of KEYBOARD is shown in section 3.4.3. As described in the requirements, asynchronous transfer of control must be used with caution. If a sequence of statements is abandoned in the middle of updating a data structure, the data might be left in an undefined state. The user must prevent this by placing such sequences in an abort-deferred region. Aborts and asynchronous transfers of control are deferred while executing a protected operation. Example: The VALIDATE_CARD procedure can use a controlled type (see 3.2.4.1) to provide automatic transaction cancellation upon abort or ATC, as follows: type TRANSACTION is new FINALIZATION_SUPPORT.CONTROLLED with record ID : TRANSACTION_ID; STATUS : TRANSACTION_STATUS := INCOMPLETE; CARD : CARD_DATA_TYPE; end record; procedure FINALIZE (TXN : in out TRANSACTION) is begin if TXN.STATUS /= COMPLETE then CANCEL_TRANSACTION(TXN); else COMMIT_TRANSACTION(TXN); end if; end FINALIZE; procedure VALIDATE_CARD(C : CARD_DATA_TYPE) is TXN : TRANSACTION; begin TXN.CARD := C; QUERY_CENTRAL_DB(TXN); TXN.STATUS := COMPLETE; -- FINALIZE(TXN) is always called at this point, -- whether procedure completes normally, or due -- to an exception, or due to an abort or ATC end VALIDATE_CARD; Discussion: QUERY_CENTRAL_DB is a procedure which communicates with a remote database. It sends a message, and waits for a reply. If this query times out, then the FINALIZE procedure for the TRANSACTION type will call CANCEL_TRANSACTION (since the STATUS will not yet be COMPLETED), which presumably sends a message to cancel the request using some information from the ID field to uniquely identify the request. Note that the decision about how long to wait before timing out is made in the caller of VALIDATE_CARD; VALIDATE_CARD can deal with time-outs because the local object TRANSACTION is a controlled type, but it does not need to determine when a time-out should occur. Example: The next example shows how asynchronous transfer of control can be used in a real-time application. CURRENT_COORDINATES is periodically updated with a new set of computed coordinates. A user task (not shown) can call READ as needed to get the most recently computed coordinates, which might then be used to control an external device. protected CURRENT_COORDINATES is procedure UPDATE (NEW_VAL : COORDINATES); -- Used by the computing task only function READ return COORDINATES; -- Used by whoever needs the result private record CURRENT_VALUE : COORDINATES; end CURRENT_COORDINATES; protected CONTROLLER is entry WAIT_FOR_OVERRUN; -- Called by the computing task procedure SET_OVERRUN; -- Called by an executive or interrupt handler private record OVERRUN_OCCURRED : BOOLEAN := FALSE; end CONTROLLER; Discussion: The CURRENT_COORDINATES protected record provides mutually exclusive access to the most recently calculated coordinates. The CONTROLLER protected record provides an entry for detecting overruns, designed to be called in a selective entry call with an abortable final part (see below). Example: Here is the CALCULATE task, which loops, progressively improving the estimate of the coordinates, until it overruns its time allotment, or the estimate stabilizes: task body CALCULATE is PROBLEM : PROBLEM_DEFN; begin GET_PROBLEM(PROBLEM); select CONTROLLER.WAIT_FOR_OVERRUN; then abort declare ANSWER : COORDINATES := INITIAL_VALUE; TEMP : COORDINATES; begin CURRENT_COORDINATES.UPDATE(ANSWER); loop -- loop until estimate stabilizes TEMP := ANSWER; TRACK_COMPUTATION.IMPROVE_ESTIMATE (PROBLEM, ANSWER); CURRENT_COORDINATES.UPDATE(ANSWER); exit when DISTANCE(ANSWER, TEMP) <= EPSILON; end loop; end; end select; end CALCULATE; Discussion: The CALCULATE task sets the value of the CURRENT_COORDINATES initially, and then repeatedly calls TRACK_COMPUTATION.IMPROVE_ESTIMATE, which is presumably a time-consuming procedure that calculates a better estimate of the coordinates. CALCULATE stops looping when it decides that the estimate has stabilized. However, it may be that IMPROVE_ESTIMATE takes too long, or the system undergoes a mode change that requires the use of the current best estimate. There is presumably an executive or interrupt handler that notices such a situation and calls CONTROLLER.SET_OVERRUN. When that happens, the CALCULATE task does an asynchronous transfer of control, ending the computation loop. Example: Here, we show a possible (partial) implementation of the IMPROVE_ESTIMATE subprogram. It depends on some work area that has a dynamic size, that can be allocated, lengthened, and deallocated. IMPROVE_ESTIMATE allocates the work area, and tries to compute the result. However, the computation of the result may fail, requiring a larger work area. Therefore, IMPROVE_ESTIMATE loops until it succeeds. package body TRACK_COMPUTATION is -- This package includes a procedure IMPROVE_ESTIMATE for -- progressively calculating a better estimate for the coordinates. -- A buffer is used for a work area to compute the new coordinates type BUFFER_SIZE is range 0 .. MAX; type BUFFER is ... type BUFFER_PTR is access BUFFER; type WORK_AREA is new FINALIZATION_SUPPORT.CONTROLLED with record BUF : BUFFER_PTR; end record; -- These procedures allocate a work area of a given size, -- and reallocate a longer work area procedure ALLOCATE_WORK_AREA( AREA : in out WORK_AREA; SIZE : in BUFFER_SIZE) is ... procedure LENGTHEN_WORK_AREA( AREA : in out WORK_AREA; AMOUNT : in BUFFER_SIZE) is ... -- This procedure is called automatically on scope exit, -- and deallocates the buffer designated by AREA.BUF. procedure FINALIZE(AREA : in out WORK_AREA) is ... procedure IMPROVE_ESTIMATE( PROBLEM : in PROBLEM_DEFN; ANSWER : in out COORDINATES) is -- Calculate a better estimate, given the old estimate. INITIAL_SIZE : BUF_SIZE := ESTIMATE_SIZE(PROBLEM); -- Compute expected work area size, based on problem definition WORK_BUFFER : WORK_AREA; begin ALLOCATE_WORK_AREA(WORK_BUFFER, INITIAL_SIZE); loop begin . . . -- Compute better estimate ANSWER := ...; exit; -- Computation succeeded exception when WORK_AREA_TOO_SMALL => -- The Problem requires a larger work area LENGTHEN_WORK_AREA( WORK_BUFFER, SIZE_INCREMENT); -- Now loop around to try again end; end loop; -- WORK_BUFFER is automatically deallocated by -- finalization on exit from the scope. end IMPROVE_ESTIMATE; end TRACK_COMPUTATION; Discussion: Since it is important that the work area be deallocated when the asynchronous transfer of control occurs, WORK_AREA is derived from FINALIZATION_SUPPORT.CONTROLLED so that a FINALIZE procedure can be defined for it (see MS-G, Systems Programming Annex). This will provide automatic clean up when the scope is exited. Note that the CALCULATE task does not (and should not) have to know about the implementation details of IMPROVE_ESTIMATE. Therefore, it is not feasible to put the call on FINALIZE(WORK_BUFFER) in CALCULATE. Furthermore, ALLOCATE_WORK_AREA might not use a normal Ada allocator. It might be allocating from some static data structure. In any case, it is important to reclaim the resources allocated to the work area when the processing is complete or aborted. Aborts and asynchronous transfers of control are deferred when a task is performing a protected subprogram or entry call, or during an initialization or finalization operation on an object of a controlled type. The programmer has complete control over the amount of code that should be placed in such abort-deferred regions. Typically, such regions should be kept short. 3.3.4. Asynchronous Communication Requirement R5.4-A(1) -- Non-Blocking Communication In Ada 9X, users can implement non-blocking communication facilities using protected records and selective entry calls. Consider the following problem: - A local server in a database system has some local knowledge, but it must occasionally query a central site for answers. - The local server must not block while waiting for room in the central mailbox, because then it might be unable to receive local requests for an extended period of time. The example solution uses a select statement with multiple entry call alternatives (see MS-9.9.2). A mailbox is used to hold requests from the client. We will reuse the mailbox abstraction presented above. In Ada 83 only a single entry call plus a delay or else alternative was possible, but for Ada 9X we have generalized the select statement to allow multiple entry call alternatives. Note that entry call and accept alternatives cannot be mixed in the same select statement; such a mixture was felt to be too complex to be implemented efficiently. Example: A database using asynchronous communication: with MAILBOX_PKG; package DB_MESSAGE is -- Define message type and mailbox type for database. type MSG_BOX; -- incomplete type type MSG_BOX_PTR is access MSG_BOX; type MSG_TYPE is record REPLY : MSG_BOX_PTR; -- Where to send reply DATA : . . . -- Rest of message end record; package MSG_BOX_PKG is new MAILBOX_PKG(MSG_TYPE, MBOX_SIZE => 10); type MSG_BOX is new MSG_BOX_PKG.MAILBOX; -- Complete the incomplete type. end DB_MESSAGE ; with DB_MESSAGE; use DB_MESSAGE; package DB_SERVER is -- This package declares the mailbox for the local server -- and the mailbox for the central server. LOCAL_MAILBOX : MSG_BOX; CENTRAL_MAILBOX : MSG_BOX; end DB_SERVER; Discussion: These package specifications define the types used for messages, and two mailboxes, one for the local server, and one for the central database server. Example: Here is the body for a typical client: with DB_SERVER; with DB_MESSAGE; use DB_MESSAGE; package body CLIENT_PKG is -- Typical client package CLIENT_MAILBOX : MSG_BOX; task CLIENT; -- Typical client task task body CLIENT is MESSAGE : MSG_TYPE; begin -- Initialize reply component of message MESSAGE.REPLY := CLIENT_MAILBOX'ACCESS; loop . . . Determine the message to be sent . . . -- Send message, wait for reply. DB_SERVER.LOCAL_MAILBOX.SEND(MESSAGE); CLIENT_MAILBOX.RECEIVE(MESSAGE); end loop; end CLIENT; end CLIENT_PKG; Discussion: A client task sends messages to its local server, via the protected record LOCAL_MAILBOX. Replies are returned to the client's mailbox, which is designated by the REPLY component of the message. Example: Here is the body for the local server: package body DB_SERVER is -- Implementation of local server type MSG_LIST_TYPE is . . . -- Used for holding messages forwarded to central server. task body LOCAL_SERVER is CLIENT_MESSAGE : MSG_TYPE; SERVER_MESSAGE_LIST : MSG_LIST_TYPE; begin loop select LOCAL_MAILBOX.RECEIVE(CLIENT_MESSAGE); -- Receive a client message. -- Respond or pass on to central server if Message can be handled locally then -- Process message -- Send processed message back to mailbox -- designated by REPLY component. CLIENT_MESSAGE.REPLY.SEND(CLIENT_MESSAGE); else -- Add message to SERVER_MESSAGE_LIST APPEND(SERVER_MESSAGE_LIST, CLIENT_MESSAGE); end if; or when not EMPTY(SERVER_MESSAGE_LIST) => -- Forward message to central server when room is -- available in central mailbox. -- (Reply sent directly back to client's mailbox.) CENTRAL_MAILBOX.SEND (HEAD(SERVER_MESSAGE_LIST).all); -- Remove head of message list. REMOVE_HEAD(SERVER_MESSAGE_LIST); end select; end loop; end LOCAL_SERVER; . . . end DB_SERVER; Discussion: The local server waits for a client message, or for room to open up in the central mailbox. When a client message arrives, the local server may either process it and respond directly, or add the message to a list of messages to be forwarded to the central server. The local server continues to process client requests, until the central mailbox has room for more messages. The replies from the central server are sent directly back to the client's mailbox, identified by the REPLY component of the message. 3.4. System Programming This section discusses various topics relating to system programming. Section 3.4.1 discusses support for unsigned integer operations and bitwise boolean operations. Section 3.4.2 discusses support for packaging and unpackaging data for transmission between different computers whose data formatting rules might be different. Section 3.4.3 discusses support for improved interrupt binding and servicing. Finally, section 3.4.4 discusses support for manipulating addresses of statically declared objects. 3.4.1. Unsigned Integer Operations Requirement R6.1-A(1) -- Unsigned Integer Operations Ada 9X satisfies the requirement for Unsigned Integer Operations by defining a standard package UNSIGNED_INTEGERS in MS-G, Systems Programming Annex. Implementation-defined unsigned types are declared in this package along with their operations. The operations include ones that treat unsigned integer values as sequences of bits. 3.4.2. Data Interoperability Requirement R6.2-A(1) -- Data Interoperability The revision requests and the Ada 9X requirements identify two different kinds of representation specifications -- internal specifications and external specifications. Internal specifications specify in-memory data representations. External specifications specify representations of data on such external media as magnetic tapes, or network connections. The existing Ada 83 representation clauses specify the internal representation. Ada 9X adds some additional representation attributes: - The T'COMPONENT_SIZE attribute is used to query or specify the size of each component of an array type. - The T'ALIGNMENT attribute is used to query or specify the memory alignment of types, subtypes, and objects. The addresses of objects whose alignment is specified must be multiples of the alignment value. In addition, in the MS-J, Information Systems Annex, a pragma LAYOUT is proposed, which would specify that the representation used by some other programming language or other standard is to be used in so far as possible in laying out the representation of a specified data type. The external representation of data is a somewhat more complex problem. Data might be represented differently due to different hardware implementations. The well known difference between ``big-endian'' and ``little-endian'' hardware is an example of such a difference. Again, data might be represented differently on the same hardware due to different software implementations. Finally, values of two structurally identical types might be represented differently due to different uses. For example, data that is frequently communicated through a network might be packed more tightly than data that is frequently accessed, and whose access must be fast. It is beyond the scope of a programming language to usurp such representation decisions. Therefore, in Ada 9X, external representation issues are programmable. Ada 9X provides the attributes T'WRITE and T'READ for converting a value of a type to and from a stream of storage elements. Default implementations are provided for all non-limited types. The default implementations may be overridden by the user associating a user-specified subprogram with the appropriate attribute (see MS-14.7.1). Example: The following program implements a Boolean matrix abstraction intended to hold the adjacency relations for a graph package. The results of computations must be communicated through a network to neighboring hosts. While computing with the matrices, the data must be represented for efficient access. However, while communicating the matrices, the data must be packed tightly. with STREAM_SUPPORT; use STREAM_SUPPORT; generic MATRIX_RANK : in NATURAL; package BOOL_MATRIX_PACKAGE type BOOL_MATRIX is array(1..MATRIX_RANK,1..MATRIX_RANK) of BOOLEAN; -- ... Some operations on Boolean_Matrices ... procedure WRITE(STREAM : STREAM_ACCESS; VALUE : BOOL_MATRIX); for BOOL_MATRIX'WRITE use WRITE; function READ(STREAM : STREAM_ACCESS) return BOOL_MATRIX; for BOOL_MATRIX'READ use READ; end BOOL_MATRIX_PACKAGE package body BOOL_MATRIX_PACKAGE is type PACKED_MATRIX is new BOOL_MATRIX; pragma PACK(PACKED_MATRIX); procedure WRITE(STREAM : STREAM_ACCESS; VALUE : BOOL_MATRIX) is begin PACKED_MATRIX'WRITE(STREAM, PACKED_MATRIX(VALUE)); -- Send a packed version of the array. end WRITE; function READ(STREAM : STREAM_ACCESS) return BOOL_MATRIX is begin return BOOL_MATRIX(PACKED_MATRIX'READ(STREAM)); -- Read from the stream, and unpack the data. end READ; end BOOL_MATRIX_PACKAGE; Discussion: In the example, BOOL_MATRIX objects are packed before writing them to the stream with the default 'WRITE for packed matrices. The READ operation reverses the WRITE operation. This example uses pragma PACK to perform the packing. Other, possibly tighter, packing methods might exist, depending on what BOOL_MATRIX values are typical. Ada 9X also extends the interface to other languages by defining three new pragmas in addition to pragma INTERFACE (see MS-13.9). 3.4.3. Interrupts Requirement R6.3-A(1) -- Interrupt Servicing Requirement R6.3-A(2) -- Interrupt Binding Protected records, in the context of the priority model, allow Ada 9X programmers to write efficient interrupt handlers. Just as in Ada 83, this solution is fully integrated into the Ada tasking model. In Ada 9X an interrupt is handled by a parameterless protected procedure declared within a library level protected record. (Ada 83 interrupt entries are retained for backward compatibility purposes, but will be moved from RM 13.5.1 to MS-G, Systems Programming Annex. The INTERRUPT_MANAGEMENT package provides the following: - The type INTERRUPT_ID, an implementation defined type used to identify interrupts that may be handled. - The sub-package INTERRUPT_NAMES, which contains implementation- defined constants (of type INTERRUPT_ID) for naming particular interrupts. - Operations for dealing with interrupts, including operations for dynamically attaching and detaching them. Example: use INTERRUPT_MANAGEMENT; ... protected type TIMER is entry WAIT_FOR_TICK; procedure HANDLE_TIMER_INTERRUPT; function GET_CLOCK return TIME_TYPE; private record CLOCK : TIME_TYPE := ...; TICK_OCCURRED : BOOLEAN := FALSE; end TIMER; ... protected type TIMER is entry WAIT_FOR_TICK when TICK_OCCURRED is begin TICK_OCCURRED := FALSE; end WAIT_FOR_TICK; procedure HANDLE_TIMER_INTERRUPT is begin ... -- Update the clock. TICK_OCCURRED := TRUE; end HANDLE_TIMER_INTERRUPT; function GET_CLOCK return TIME_TYPE is begin return CLOCK; end GET_CLOCK; end TIMER; ... MY_TIMER : TIMER; ... ATTACH_HANDLER (MY_TIMER.HANDLE_TIMER_INTERRUPT'ACCESS, INTERRUPT_NAMES.TIMER_INTERRUPT_ID); ... Discussion: The ATTACH_HANDLER procedure associates the HANDLE_TIMER_INTERRUPT procedure with the interrupt assigned to TIMER_INTERRUPT_ID. The Ada 9X priority model, and the properties of protected records, guarantee that when the interrupt occurs the procedure can be invoked directly from the interrupt vector. The mutual exclusion property of protected records implies that the interrupt is prevented from occurring while a protected operation of TIMER is executing. Example: An implementation of KEYBOARD, where one of the procedures of the protected record is attached to an interrupt handler: protected KEYBOARD is entry CANCEL_PRESSED; -- Wait for cancel key-stroke procedure HANDLE_INTERRUPT; pragma ATTACH_HANDLER(HANDLE_INTERRUPT, KEYBOARD_INTERRUPT_ID); . . .; private record LAST_KEY_PRESSED : KEYBOARD_CHARACTERS; BUFFER : MESSAGE_BUFFER; -- Sequence of keyboard characters end KEYBOARD; protected body KEYBOARD is entry CANCEL_PRESSED when LAST_KEY_PRESSED = CANCEL_KEY; procedure HANDLE_INTERRUPT is begin ... -- Put characters in buffer and set LAST_KEY_PRESSED. end HANDLE_INTERRUPT; end KEYBOARD; Interrupt handlers are associated with protected record objects and not with protected types. Consequently, multiple protected record objects of the same type can each be associated with a different interrupt source. Interrupts are associated with the implementation defined type INTERRUPT_ID, and not with SYSTEM.ADDRESS. This means that interrupt identification may be made in the most convenient manner for the particular hardware. 3.4.4. Dynamic References Requirement R6.4-A(1) -- Access Values Designating Global Objects Ada 9X meets this requirement by allowing access values to designate declared objects. In Ada 9X there are two steps to using such values. - Objects that are to be designated by access values must be declared with the reserved word aliased. This serves two purposes. First, it documents the fact that an object is to be designated by an access value. Second, it forces the object to be properly aligned in memory. - The attributes 'ACCESS and 'UNCHECKED_ACCESS, when applied to objects, return an access value that designates the object. The access values which are formed by the ACCESS attribute must obey certain scope restrictions, which are checked at compile time. They cannot be assigned to objects whose lifetime is longer than the lifetime of the designated object. The access values that are formed by the UNCHECKED_ACCESS attribute are not subject to such restrictions. It is the responsibility of the programmer who uses such unchecked access values to avoid dangling references. The user need also identifies the fact that the data may be preallocated by a mechanism other than the Ada program. Pragma INTERFACE_OBJECT allows the user to identify the object as one that is external to the Ada program (see MS-13.9). Example: Static Ragged Arrays package MESSAGE_SERVICES is type MESSAGE_CODE_TYPE is range 0..100; subtype MESSAGE is STRING; function GET_MESSAGE (MESSAGE_CODE: MESSAGE_CODE_TYPE) return MESSAGE; pragma INLINE (GET_MESSAGE); end MESSAGE_SERVICES; package body MESSAGE_SERVICES is type MESSAGE_HANDLE is access constant MESSAGE; MESSAGE_0 : constant aliased MESSAGE := "OK"; MESSAGE_1 : constant aliased MESSAGE := "UP"; MESSAGE_2 : constant aliased MESSAGE := "SHUTDOWN"; . . . MESSAGE_TABLE : array (MESSAGE_CODE_TYPE) of MESSAGE_HANDLE := (0 => MESSAGE_0'ACCESS, 1 => MESSAGE_1'ACCESS, 2 => MESSAGE_2'ACCESS, -- etc. ); function GET_MESSAGE (MESSAGE_CODE: MESSAGE_CODE_TYPE) return MESSAGE is begin return MESSAGE_TABLE(MESSAGE_CODE).all; end GET_MESSAGE; end MESSAGE_SERVICES; Discussion: The example, based on Revision Request 018, declares a static ragged array. The elements of the array point to strings, the lengths of which may differ. The access values are generated by the 'ACCESS attribute; no dynamic allocation is needed to create the values. Study Topic S6.4-B(1) -- Low-Level Pointer Operations Ada 9X solves the issues raised in this Study Topic by adding the following three new operations to package SYSTEM (see MS-13.7). - An operation to add STORAGE_OFFSET to ADDRESS, yielding a new SYSTEM.ADDRESS. - An operation to subtract STORAGE_OFFSET from ADDRESS, yielding a new ADDRESS. - An operation to subtract two addresses, yielding a STORAGE_OFFSET. 3.5. Parallel Processing This section discusses topics related to programming parallel architectures. Section 3.5.1 discusses Ada 9X features for controlling shared memory and configuring programs for multi-processor systems. Section 3.5.2 describes the Ada 9X solutions to problems posed by massively parallel and vector architectures. 3.5.1. Shared Memory Requirement R7.1-A(1) -- Control of Shared Memory Ada 9X satisfies the requirement for accessing shared memory by defining two new pragmas which can be used in object and component declarations: ATOMIC and VOLATILE. (See MS-G, Systems Programming Annex.) The classic examples are the shared circular buffer and memory mapped I/O [Dewar 90]. Example: -- Shared data items type INDEX is range 0..SIZE-1; type BUFFER is array(INDEX) of CHARACTER; DATA : BUFFER; pragma VOLATILE(DATA); IN_INDEX, OUT_INDEX: INDEX := 0; pragma ATOMIC(IN_INDEX); pragma ATOMIC(OUT_INDEX); . . . -- Code in the producer task while (IN_INDEX+1) mod SIZE = OUT_INDEX loop -- wait for slot null; end loop; DATA((IN_INDEX+1) mod SIZE) := ELEMENT; -- put data in IN_INDEX := (IN_INDEX + 1) mod SIZE; . . . -- Code in the consumer task while IN_INDEX = OUT_INDEX loop -- wait if empty null; end loop; ELEMENT := DATA(OUT_INDEX); OUT_INDEX := (OUT_INDEX + 1) mod SIZE; Discussion: This example illustrates how the pragmas ATOMIC and VOLATILE can be used to synchronize two tasks. By declaring DATA volatile, the user indicates that no local copies can be held between statements. By declaring IN_INDEX and OUT_INDEX atomic, the user insures that no local copies will be kept, and that their load and store operations will be indivisible. The ATOMIC pragma is useful when the only indivisible operations required for the type are load and store, and when the type is such that the hardware supports these operations indivisibly. For large types, or where more complex synchronization is required, protected records can be used. Example: Below we present a type declaration that might be used to implement memory-mapped I/O in Ada 9X: type IO_REC_TYPE is record START_ADDRESS : SYSTEM.ADDRESS; pragma VOLATILE(START_ADDRESS); LENGTH : INTEGER; pragma VOLATILE(LENGTH); OPERATION : OPERATION_TYPE; pragma ATOMIC(OPERATION); RESET : BOOLEAN; pragma ATOMIC(RESET); end record; -- A store into the OPERATION field triggers an I/O operation. -- Reading the RESET field terminates the current operation. IO_REC : IO_REC_TYPE; for IO_REC use at ... ; Discussion: By using pragmas to indicate the sharability of data, the semantics of reading and writing components can be controlled. By declaring OPERATION and RESET atomic, the user ensures that reads and writes of these fields are not removed by optimization, and are performed indivisibly. By declaring START_ADDRESS and LENGTH volatile, the user forces any store to happen immediately, without the use of local copies. 3.5.2. Massively Parallel Architectures Study Topic S7.2-A(1) -- Managing Large Numbers of Tasks Ada 9X allows tasks to be parameterized by discriminants. This allows large numbers of tasks to be informed of their identity, or to be passed their initial inputs, without requiring that each first rendezvous with a controlling task. Example: Initiating an array of tasks in parallel, parameterized by a discriminant: type TASK_RANGE is range 1..1000; type INITIAL_DATA_TABLE_TYPE is array(TASK_RANGE) of FLOAT; INITIAL_DATA : INITIAL_DATA_TABLE_TYPE := ( ... ); task type COMPUTE_CELL(INDEX : TASK_RANGE) is entry RESULT(VALUE : out FLOAT) ; end COMPUTE_CELL; type COMPUTE_CELL_PTR is access COMPUTE_CELL; type COMPUTATION_ARRAY_TYPE is array(TASK_RANGE) of COMPUTE_CELL_PTR; COMPUTATION_ARRAY : COMPUTATION_ARRAY_TYPE := (for I in TASK_RANGE => new COMPUTE_CELL(I)); Discussion: This example illustrates the creation and activation of 1000 tasks. Each task can use its INDEX to look up its initial data or to find its neighboring tasks in COMPUTATION_ARRAY. In the example, COMPUTATION_ARRAY is initialized with an array aggregate. Instead of requiring the user to write out a separate expression for each component, Ada 9X allows an array index to be given a name in a component association (in this case, I), which can be used in component expressions. 3.5.3. Vector Architectures Study Topic S7.3-A(1) -- Statement Level Parallelism Ada 9X defines two levels of ``sensitivity'' to exceptions (see MS-11.6). A ``precise'' mode is necessary to meet the requirements of safety critical and trusted systems. An ``imprecise'' mode is intended to meet the needs of heavily pipelined and vector architectures. Ada's array aggregates have always provided a limited form of statement-level parallelism. The ``named index'' feature (see MS-4.3.2) allows this capability to be used more conveniently in Ada 9X. Example: The following code fragment could be compiled into a vector addition: declare type VECTOR is array(VECTOR_RANGE) of FLOAT; A,B,C : VECTOR; begin -- In Ada 83 a vector addition can be expressed as: C := (1 => A(1) + B(1), 2 => A(2) + B(2), 3 => A(3) + B(3), ...); -- etc... until VECTOR_RANGE'LAST. -- In Ada 9X the same vector addition can be expressed as: C := (for I in VECTOR_RANGE => A(I) + B(I)); end; Discussion: In each case, the right hand side of the assignment statement is an array aggregate. The nth component is the sum of the nth components from A and B. The language allows all components to be evaluated in any order, allowing this statement to be compiled into a vector addition if the component expressions have no side-effects. 3.5.4. Configuration of Parallel Programs Study Topic S7.4-A(1) -- Configuration of Parallel Programs None of the proposals in the current version of the Mapping Specification address this study topic. We expect that implementation-defined pragmas or attributes will be adequate, and that mandating a standard approach would not have sufficient user benefit. 3.6. Distributed Processing Requirement R8.1-A(1) -- Facilitating Software Distribution The basic principles of distributing a program in Ada 9X are: - The unit of distribution is a set of packages and subprograms, called a partition. - The determination of which packages belong to which partition is performed at link time. The mechanism for specifying a link-time partitioning is implementation-defined. - Communication between partitions is accomplished via REMOTE_CALL_INTERFACE packages, or (if the architecture supports shared memory) through variables declared in SHARED_PASSIVE packages. - The communication subsystem used to implement remote calls is provided by the user or by a third party vendor. Ada 9X specifies a standard interface to the subsystem. - The mapping of partitions to physical nodes is determined by a user- or implementation-provided configuration subsystem. - There is an optional capability to support dynamic binding between partitions based on either access-to-subprogram types or access-to- tagged types. This approach avoids the danger of overspecifying the behavior of distributed programs, and gives the compiler implementer a convenient way to support diverse distributed architectures and communication subsystems. There was a strong desire that the interface to the user provided communication subsystem (UPCS) be as simple as possible. To this end, task, protected record, access type, and object declarations are not allowed in REMOTE_CALL_INTERFACE packages. These restrictions allow implementations to avoid the complexity of a distributed tasking run-time system. The limitations imposed by these restrictions can be worked around by programmers with a need to do so. A remote procedure call with an extra duration parameter can be used to implement a timed call to another partition. Asynchronous transfer of control can be used to time-out a remote procedure call, using the local clock. Example: A timed remote procedure call. select delay 0.1; raise REMOTE_CALL_TOOK_TOO_LONG; then abort -- This is a remote call RCI.READ_RECORD( ... ) ; end The Ada 9X approach to distribution is similar to the virtual nodes approach [Atkinson 88] as well as the commonly used practice of building distributed systems out of multiple Ada 83 programs. Over the latter approach, it has the advantage of having strongly type-checked interfaces between partitions, since, in Ada 9X, the distributed system is a single program. The communication subsystem, including the semantics of communication failure, is implemented by the user. A predefined exception COMMUNICATION_ERROR is provided for use by the UPCS to signal problems. Example: The following is an example of a database server, where the clients have their own address spaces, but there is some globally shared memory usable for holding messages passed between the clients and the database server. It illustrates the use of PURE, SHARED_PASSIVE, and REMOTE_CALL_INTERFACE packages. First we define types to be used for communicating between partitions: package COMM_TYPES is pragma PURE; -- Define some types used for communication -- This package can be shared by all active partitions that -- depend on it. type FILE_MODE is (IN_DB, OUT_DB, INOUT_DB); type BYTE is range 0..255; RECORD_SIZE : constant := 4096; -- Max Record size in bytes type BYTE_INDEX is range 0..RECORD_SIZE-1; -- Byte index within record type RECORD_KEY is new STRING(1..32); -- Record key type RECORD_BUFFER_TYPE is array(BYTE_INDEX) of BYTE; -- Record-size array of bytes. pragma PACK(RECORD_BUFFER_TYPE); -- Seconds since beginning of epoch type SYSTEM_TIME is range 0..2**31-1; end COMM_TYPES; Discussion: Package COMM_TYPES is a PURE package, allowing it to be replicated by all partitions that depend on it. The fact that there is no state in the package, or in any package on which it depends, allows it to be replicated (or not) without changing the meaning of the program. Example: Here we define the interface to the passive partition corresponding to a global shared buffer pool: with COMM_TYPES; package GLOBAL_MEMORY is pragma SHARED_PASSIVE; -- Package for Passive Partition, to be in some -- shared memory module GLOBAL_TIMER : COMM_TYPES.SYSTEM_TIME; pragma ATOMIC(GLOBAL_TIMER); -- Timer in global memory, updated/referenced atomically NUM_BUFFERS : constant := 100; -- Number of global buffers type BUFFER_INDEX is range 0..NUM_BUFFERS := 0; NULL_BUFFER_INDEX : constant BUFFER_INDEX := 0; -- Array of global buffers BUFFER_ARRAY : array(BUFFER_INDEX range 1 .. NUM_BUFFERS) of COMM_TYPES.RECORD_BUFFER_TYPE; -- Array used to keep free list type INDEX_ARRAY is array(BUFFER_INDEX range 1 .. NUM_BUFFERS) of BUFFER_INDEX; protected BUFFER_POOL_MANAGER is -- Singleton protected record for -- managing the global buffer pool; -- must be "entry-less" to be used in shared memory. procedure GET_BUFFER(BUFFER : out BUFFER_INDEX); -- Returns Null_Buffer_Index -- if no global buffers available. procedure RELEASE_BUFFER(BUFFER : in out BUFFER_INDEX); -- Sets param to Null_Buffer_Index -- after releasing buffer -- back to global buffer pool private record LAST_USED_BUFFER : BUFFER_INDEX := 0; -- Index of last buffer ever allocated FIRST_FREE_BUFFER : BUFFER_INDEX := 0; -- Index of first buffer on free list NEXT_FREE_BUFFER : INDEX_ARRAY; -- Index of next buffer on free list; -- parallel to buffer array. end BUFFER_POOL_MANAGER; end GLOBAL_MEMORY; Discussion: Package GLOBAL_MEMORY is a SHARED_PASSIVE package, allowing it to be placed in a passive partition accessible to all active partitions that directly depend upon it. The protected record is used to synchronize access to the list of free buffers. (Only protected records without entries may be declared in SHARED_PASSIVE packages. See MS-I, Distributed Systems Annex.) Example: Here we define the primary interface to the database server. It provides two READ/WRITE interfaces, one that sends all data via parameters, and one that uses the buffer pool for communicating some of the data: with COMM_TYPES; use COMM_TYPES; with GLOBAL_MEMORY; package DATABASE_SERVER_INTERFACE is pragma REMOTE_CALL_INTERFACE; -- Interface to remote active partition package GLOB renames GLOBAL_MEMORY; type SERVER_DB_HANDLE is private; -- Call server to open a database procedure OPEN(HANDLE : out SERVER_DB_HANDLE; DATABASE : in STRING); -- Call server to read a record from a database procedure READ_RECORD(HANDLE : in SERVER_DB_HANDLE; KEY : in RECORD_KEY; DATA : out BUFFER_TYPE; LAST : out BYTE_INDEX); -- Call server to write a record to a database procedure WRITE_RECORD(HANDLE : in SERVER_DB_HANDLE; KEY : in RECORD_KEY; DATA : in BUFFER_TYPE; LAST : in BYTE_INDEX := BYTE_INDEX'LAST); -- Call server to read into a global buffer procedure READ_INTO_BUFFER(HANDLE : in SERVER_DB_HANDLE; KEY : in RECORD_KEY; BUFFER : in GLOB.BUFFER_INDEX; LAST : out BYTE_INDEX); -- Call server to write from global buffer procedure WRITE_FROM_BUFFER(HANDLE : in SERVER_DB_HANDLE; KEY : in RECORD_KEY; BUFFER : in GLOB.BUFFER_INDEX; LAST : in BYTE_INDEX := BYTE_INDEX'LAST); private type SERVER_DB_HANDLE is range 0..2**31-1; -- Scalar private type end DATABASE_SERVER_INTERFACE; Discussion: Package DATABASE_SERVER_INTERFACE is a REMOTE_CALL_INTERFACE to the active partition that implements the database server. It may only depend on PURE and SHARED_PASSIVE packages or other REMOTE_CALL_INTERFACE packages. Example: Here we define a normal, ``per-partition'' DB_UTILITIES package, which provides a higher level interface to the database server. It provides some amount of local caching of information, stronger type checking, and allows one call on this package to result in zero or more remote subprogram calls on the server: with DATABASE_SERVER_INTERFACE; with FINALIZATION_SUPPORT; package DB_UTILITIES is procedure COPY_DATABASE(FROM, TO : STRING); -- Opens FROM and creates TO, copying all records -- This will involve several calls to the -- Remote_Database_Interface package. type LOCAL_DB_HANDLE is new FINALIZATION_SUPPORT.CONTROLLED with limited private; -- This database handle is kept locally, and provides -- local buffering and automatic close on scope exit. -- Open a remote database procedure OPEN(HANDLE : in out LOCAL_DB_HANDLE; DATABASE : in STRING); -- Close remote database and local handle procedure CLOSE(HANDLE : in out LOCAL_DB_HANDLE); -- Define the finalization operation procedure FINALIZE (HANDLE : in out LOCAL_DB_HANDLE) renames CLOSE; generic -- Generic to do typed fetch/insert of records. type ITEM_TYPE is private; package GENERIC_RECORD_OPERATIONS is procedure FETCH(HANDLE : in LOCAL_DB_HANDLE; KEY : in RECORD_KEY; DATA : out ITEM_TYPE); procedure INSERT(HANDLE : in LOCAL_DB_HANDLE; KEY : in RECORD_KEY; DATA : in ITEM_TYPE); end GENERIC_RECORD_OPERATIONS; private type LOCAL_DB_RECORD; type LOCAL_DB_HANDLE is new FINALIZATION_SUPPORT.CONTROLLED with record LOCAL_DB : LOCAL_DB_RECORD; end record; -- Local_DB_Record will give access -- to a local cache to reduce the number of -- read-record/write-record messages. end DB_UTILITIES; Discussion: Package DB_UTILITIES is a normal, per-partition package which will be included in all active partitions that reference it. A separate copy of the local variables of the package is allocated and elaborated for each partition that includes the package. The LOCAL_DB_HANDLE type is a controlled type, so the FINALIZE procedure will be called whenever a scope containing a LOCAL_DB_HANDLE variable is left. The FINALIZE procedure will make sure that the remote database is closed and deallocate any resources. Requirement R8.2-A(1) -- Dynamic Reconfiguration Each active partition has its own elaboration sequence, and, optionally, its own main subprogram. Partitions may be deleted and created during the execution of a program, although the specific mechanism is not defined by the language standard. Packages are partitioned after compilation by assigning each package a partition ID and then building the partitions, so it is possible to build different configurations by assigning packages to different partitions. However, using this technique, the binding between partitions is known at link time. The current version of the Mapping Specification proposes two approaches for providing dynamic binding between partitions (see MS-I, Distributed Systems Annex), one based on access-to-subprograms, and one based on access-to-tagged-types. 3.7. Safety-Critical and Trusted Applications Study Topic S9.1-A(1) -- Determining Implementation Choices Ada 9X classifies the set of incompletely specified behaviors as erroneous, bounded error, implementation dependent, and implementation defined (MS-1). Implementations are required to document all implementation defined behavior. Certain erroneous conditions in Ada 83 have been reclassified as implementation dependent in Ada 9X, and others have been reclassified as bounded errors. This reduces the set of allowable outcomes for programs that contain such conditions. The erroneous and bounded error situations will be enumerated (see MS-K, Safety and Security Annex). Requirement R9.1-A(2) -- Ensuring Canonical Application of Operations Ada 9X specifies that there are two modes that must be supported by compilers. Under the ``precise'' mode the canonical order of operations must be preserved (see MS-11.6). Requirement R9.2-A(1) -- Generating Easily Checked Code The Safety and Security Annex introduces a pragma TRACEABLE_OBJECT_CODE, which requires that the generated code be easy to check. If this pragma is applied to a unit, the generated code must either have an obvious relationship to the Ada code, or additional information must be provided to make the relationship traceable (see MS-K, Safety and Security Annex). Requirement R9.3-A(1) -- Allow Additional Compile-Time Restrictions In Ada 9X, implementations are allowed to have modes that reject legal Ada programs (MS-1.6). Such modes can be used to enforce additional compile time restrictions deemed necessary for programming safety critical systems. However, all implementation must support a mode where all legal programs are accepted. 3.8. Information Systems 3.8.1. Decimal Based Types Requirement R10.1-A(1) -- Decimal-Based Types Study Topic S10.1-A(2) -- Specification of Decimal Representation Requirement R10.3-A(1) -- Interfacing with Data Base Systems A new class decimal fixed-point types will be added to the core language, as a special case of general fixed-point types. A Decimal Fixed-Point type: type EASY_MONEY is delta 0.01 digits 10; -- -99_999_999.99 .. 99_999_999.99 Information Systems-compliant implementations will be required to provide data representation and computational support for these types. (See MS-J, Information Systems Annex, and its rationale for further details.) 3.8.2. Common Functions Study Topic S10.4-A(1) -- Varying-Length String Package By eliminating the Ada 83 restriction that prevents discriminants on array types (see MS-3.6.1), Ada 9X supports a simple varying length string type. Example: Using a discriminated array type to provide varying length strings: type VSTRING_LENGTH is INTEGER range 0..255; type VSTRING(LENGTH : VSTRING_LENGTH := 0) is new STRING(1..LENGTH); -- A varying length string that can hold a -- maximum of 255 characters. Discussion: Unconstrained variables of type VSTRING may be declared, because of the default for the discriminant. Any VSTRING value, from length 0 to 255 characters, may be assigned to such a variable. VSTRING is derived from STRING, and hence values of the type may be converted to STRING for passing to a subprogram which expects a STRING as a parameter (such as TEXT_IO.PUT_LINE). Study Topic S10.4-A(2) -- String Manipulation Functions The Information Systems Annex in the current Mapping Specification contains several specifications for generic string handling packages to satisfy this requirement. Study Topic S10.2-A(1) -- Alternate Character Set Support The Information Systems Annex provides package specifications for alternate character sets including the international character sets LATIN-N, and EBCDIC. Note: Additional discussion of Information Systems support may be found in MS-J, Information Systems Annex, and its accompanying rationale. 3.9. Scientific and Mathematical Applications Requirement R11.1-A(1) -- Standard Mathematics Packages The Numerics Annex will include the proposed ISO standard Generic Elementary Functions. These functions will be provided as a package called GENERIC_ELEMENTARY_FUNCTIONS. The proposed ISO standard Generic Primitive Functions will also be included in the Numerics Annex. In this case, however, they will be provided as attributes rather than as functions in a generic package. (See MS-L, Numerics Annex.) Study Topic S11.1-B(1) -- Floating Point Facilities The Numerics Annex documents the Ada 9X model of floating point arithmetic. The floating point model has been simplified by eliminating Ada 83 model numbers, and by allowing safe numbers to reflect the characteristics of the hardware more closely (see MS-L, Numerics Annex). Study Topic S11.2-A(1) -- Array Representation In Ada 9X the component size of an array can be specified by an attribute definition clause (see MS-13.2). The Information Systems Annex also proposes a pragma LAYOUT to specify that an array type representation should conform to that of some of other programming language (e.g. Fortran) (see MS-J, Information Systems Annex). 3.10. General Requirements This section addresses issues related to the general requirements. It discusses the changes to Ada which make the language more efficient to implement, and simpler to understand overall. 3.10.1. Efficiency, Simplicity, and Consistency To address the requirements for efficiency, simplicity, and consistency, some number of smaller changes have been proposed for Ada 9X. These changes have been made to unify rules, reduce the number of distinct concepts, eliminate effects which negatively impact the efficiency of generated code, and remove odd anomalies discovered after the standardization of Ada 83. The overall effect of these changes is a core language which we believe will be easier to teach, easier to learn, and simpler to use than Ada 83. Requirement R2.2-A(1) -- Reduce Deterrents to Efficiency The Requirements Rationale [DoD 91] identifies two deterrents to efficiency, super-null arrays and returning local tasks as function results. As a partial simplification in the handling of null arrays, MS-4.5.3 specifies that the low bound of a catenate is determined by the low bound of the left operand, independent of whether or not it is null. The issue of returning local tasks is resolved by specifying in MS-7.4.5 that when a function returns a value of a limited type, the function is not ``left'' until the returned value is no longer accessible. So, if a function returns a local task object, the function is not left until the returned task is no longer accessible. At that point any storage associated with the task can be reclaimed. To further enhance efficiency, Ada 9X refines the definition of a static expression to allow more kinds of compile-time known expressions (see MS-4.9). Requirement R2.2-B(1) -- Understandability The Requirements Rationale section 12.2 identifies several areas where changes to Ada might reduce common errors due to programmer confusion. Ada 9X incorporates the following changes to minimize common programming errors: - In Ada 9X the distinction between earlier and later declarative items has been eliminated. Declarations of any sort may occur at any point in a declarative region (see MS-3.10). - In Ada 9X pragma ELABORATE is transitive (see MS-10.5). - In Ada 9X character literals and the primitive operators for a type are directly visible everywhere in a manner analogous to the basic operations such as assignment. This allows infix notation to be used without a use clause naming the package where the operators are declared (see MS-8.3). - In Ada 9X all library unit packages must have a separate specification and body (see MS-10.1). Requirement R2.2-C(1) -- Minimize Special-Case Restrictions The Requirements Rationale section 12.3 identifies a number of areas where special case restrictions might be eliminated from the language. These are addressed in Ada 9X, as follows: - In Ada 9X any composite type may have discriminants (see MS-3.6.1). - Redefinition of "=" is allowed for all types (see MS-6.7). - Parameters of mode out may be read after being initialized (see MS-6.2). - Negative literals may be used to specify the bounds of a loop (see MS-3.7.1). Study Topic S2.3-A(1) -- Improve Early Detection of Errors This study topic is addressed by the following proposals: - Allowing a mode where warnings signal rejection (see MS-1.6). - Compile time checking on 'ACCESS (MS-3.9.2). Requirement R2.3-A(2) -- Limit Consequences of Erroneous Execution This requirement is addressed by the following proposal: - Definitions of ``erroneous'' and ``bounded errors'' (see MS-1.6). 3.11. Summary This chapter has demonstrated that the proposed Ada 9X mapping meets the Ada 9X requirements. In the next chapter, the three primitive building blocks on which the Ada 9X mapping is based are described in depth. 4. In-Depth Analyses of Major Enhancements 4.1. Introduction Ada 9X is based on a building block approach. Rather than proposing a number of new language features to directly solve each identified application problem, the Mapping/Revision Team has proposed a few primitive language building blocks. In combination, these building blocks enable programmers to solve more application problems efficiently and productively. For example, the Ada 9X Requirements identifies issues concerning Common Real Time Paradigms in general (R5.2-A(2)) and Asynchronous Communication (R5.4-A(1)) in specific. Rather than adding new standard language mechanisms to support each important real time paradigm, the Ada 9X solution is built upon two new building blocks, protected records (MS-9.5,6,7) and selective entry calls (MS-9.9.2). Together, these two building blocks may be used to program natural and efficient solutions to problems involving a myriad of real-time paradigms, including asynchronous communication, efficient mutual exclusion, barrier synchronization, counting semaphores, and broadcast of a signal. This chapter discusses the three areas where we have proposed new building blocks for Ada: Object-Oriented Programming Type Extension (MS-3.4.1), Class-wide Types (MS-3.3) Programming-In-The-Large Child Library Units (MS-10.1) Real-Time Programming Protected Records (MS-9.5,6,7) In each of these three areas, we will provide a brief overview of the proposed new building blocks, in the context of the requirements and existing practice. The overview will be followed by a longer discussion illustrating through examples the intended use of the features. The intent of this chapter is to introduce the reader to the most important new capabilities of Ada 9X, using a relatively informal expository style. Where appropriate, references to the Mapping Specification are provided, to allow the reader to investigate the details of the proposals in more depth. 4.2. Object-Oriented Programming 4.2.1. Purpose and Requirements Satisfied Ada 9X provides support for the paradigm of object-oriented programming (OOP). This section describes Ada 9X's mechanisms for supporting this paradigm, demonstrates idioms for their use, and contrasts the Ada 9X approach with those of other object-oriented programming languages. Ada has been traditionally associated with object-oriented design [Booch 86], which advocates the design of systems in terms of abstract data types using objects, operations on objects, and their encapsulation as private types within packages. The ``ingredients'' of object-oriented design may be summarized as follows: Objects Entities that have structure and state. Operations Actions on objects that may access or manipulate that state. Encapsulation Some means of defining objects and their operations and providing an abstract interface to them, while hiding their implementation details. Ada 83 is well-suited to supporting the paradigm of object-oriented design. Object-oriented programming, as that term has evolved over the past decade, builds upon the base of object-oriented design, adding two other ingredients: inheritance and polymorphism. While the specific properties of these two facilities vary from one programming language to another, their essential characteristics may be stated this way: Inheritance A means for incrementally building new abstractions from existing ones by ``inheriting'' their properties -- without disturbing the original abstractions' implementations or existing clients. Polymorphism A means of factoring out the differences among a collection of abstractions, such that programs may be written in terms of their common properties. Ada 83's support for inheritance and polymorphism is less systematic than that found in fully object-oriented languages (see 4.2.4). Recognizing this, the Ada 9X Requirements reflect the need to provide improved support for this paradigm in several ways [DoD 90]: Study Topic S4.1-A(1) -- Subprograms as Objects: Ada 9X should provide: 1. an easily implemented and efficient mechanism for dynamically selecting a subprogram that is to be called with a particular argument list; 2. a means of separating the set of subprograms that can be selected dynamically from the code that makes the selection. Study Topic S4.3-A(1) -- Reducing the Need for Recompilation: Ada 9X recompilation and related rules should be revised so it is easier for implementations to minimize the need for recompilation and for programs to use program structures that reduce the need for recompilation. Study Topic S4.3-B(1) -- Programming by Specialization/Extension: Ada 9X shall make it possible to define new declared entities whose properties are adapted from those of existing entities by the addition or modification of properties or operations in such a way that: - the original entity's definition and implementation are not modified; - the new entity (or instances thereof) can be used anywhere the original one could be, in exactly the same way. Each of these can be understood in relation to object-oriented programming. S4.1-A(1) seeks the ability to associate operations (subprograms) with objects, and to dynamically select and execute those operations. This is one basis on which to develop run-time polymorphism. Among the various causes of excessive recompilation addressed by S4.3-A(1) are those arising from the breakage of an existing abstraction for the purpose of extending or otherwise reusing it to build a new abstraction. The topic S4.3-B(1) implies the essence of object-oriented programming as defined above. Alternatively, one might think of this in terms of two programming paradigms: Variant programming new abstractions may be constructed from existing ones such that the programmer need only specify the differences between the new and old abstractions. Class-wide programming classes of related abstractions may be handled in a unified fashion, such that the programmer may systematically ignore their differences when appropriate. 4.2.2. Brief Description Ada 9X generalizes Ada 83's type facilities to provide more powerful mechanisms for variant and class-wide program development and composition. Derived types in Ada 83 provide a simple inheritance mechanism: derived types inherit exactly the structure, operations, and values of their parent type. This ``inheritance'' may be augmented with additional operations. Ada 9X generalizes type derivation to permit type extension (MS-3.4.1). A tagged record or private type may be extended with additional components as a part of derivation. Tagged types offer Ada programmers a mechanism for single inheritance as found in object-oriented programming languages such as Simula [Birtwistle 73] and Smalltalk [Goldberg 83]. Example of Type Extension (inspired by [Seidewitz 91]): type ACCOUNT_WITH_INTEREST is tagged record IDENTITY : ACCOUNT_NUMBER := NONE; BALANCE : MONEY := 0.00; RATE : INTEREST_RATE := 0.05; INTEREST : MONEY := 0.00; end record; procedure ACCRUE_INTEREST( ON_ACCOUNT : in out ACCOUNT_WITH_INTEREST; OVER_TIME : in INTEGER); procedure DEDUCT_CHARGES (FROM : in out ACCOUNT_WITH_INTEREST); . . . type FREE_CHECKING_ACCOUNT is new ACCOUNT_WITH_INTEREST with record MINIMUM_BALANCE : MONEY := 500.00; TRANSACTIONS : NATURAL := 0; end record; procedure DEPOSIT( INTO : in out FREE_CHECKING_ACCOUNT; AMOUNT : in MONEY); procedure WITHDRAW( FROM : in out FREE_CHECKING_ACCOUNT; AMOUNT : in MONEY); INSUFFICIENT_FUNDS : exception; -- raised by WITHDRAW procedure DEDUCT_CHARGES (FROM : in out FREE_CHECKING_ACCOUNT); ACCOUNT_WITH_INTEREST is a tagged type. The type FREE_CHECKING_ACCOUNT is derived from it, inheriting copies of its components (IDENTITY, BALANCE, RATE, INTEREST) and its derivable operations (ACCRUE_INTEREST and DEDUCT_CHARGES). The derived type declaration has a record extension part that adds two additional components (MINIMUM_BALANCE, TRANSACTIONS) to those inherited from the parent and adds some new operations. It would also override DEDUCT_CHARGES such that if the BALANCE was above the MINIMUM_BALANCE, no charges would be deducted. All components of the type, whether inherited or declared as a part of the extension, are equally accessible (unlike ``nested'' record types). In Ada 83, the types declared in the visible part of a package have special significance for Ada's abstraction mechanisms, Such operations on user-defined types are first-class in a manner that parallels those of the predefined types. For derived types, these operations, together with the implicitly declared basic operations, were the derivable operations on a type. With the increased importance of derived types for object-oriented programming in Ada 9X, the notion of the operations closely related to a type in this manner is generalized. The primitive operations of a type are those that are implicitly declared for the type and those subprograms that have an operand or result of the type and that are declared immediately within the same list of declarations as the type itself (MS-3.3.3). In Ada 9X, the derivable operations of Ada 83 have become ``primitive operations'' and the restriction of these operations to the visible part of a package has been eliminated. These changes support added capability: primitive operations may be private and a type and its derivatives may be declared in the same declarative region (which is often quite useful for building related abstractions). (See 4.2.3 for further discussion.) Primitive operations clarify the notion of an abstract data type for purposes of object-oriented programming (inheritance and polymorphism), generic units and visibility. They are distinguished from the other operations of a type in the following ways: Inheritance Primitive operations are the derivable (inherited) operations. Polymorphism Primitive operations are dispatching operations on tagged types. Generic Units Primitive operations are the ones available within generic templates parameterized by a ``class''. Operator Visibility Operators that are primitive operations (and character literals) are primitively visible -- they are visible where their type is, without requiring a use clause (MS-8.3), like basic operations. Ada 83 used the term ``class'' (see RM 3.3) to characterize collections of related types. A type's class determines how the type is declared, what types it is interconvertible with, its predefined operations, and structure. The class of a generic formal type parameter determines the operations that are available within the generic template. Types within a class have common structure and operations (see 2.1.2 for a description of Ada's class structure). Ada 9X formalizes the Ada 83 notion of class as follows: A type and its direct and indirect derivatives, whether or not extended, constitute a class. This definition allows for user-defined classes based on derivation. User-defined classes, like their language-defined counterparts, support type conversion, may be used to parameterize generic units, and provide a new kind of class-wide programming. Explicit conversion is defined among types within a class, as it is in Ada 83 for types related by derivation. Conversions account for possibly differing components between the source and target types as follows: Conversions toward the root of the hierarchy ``forget'' any components of the source type that are not defined for the target type. Conversions away from the root must allow for the possibility of additional components in the target type. Such components are provided in a conversion using named associations (MS-4.6). Conversions between specific tagged types are constructor conversions. Continuing the previous example, an ACCOUNT_WITH_INTEREST can be converted to a FREE_CHECKING_ACCOUNT, providing a value for minimum balance and the number of transactions. Example: ACCOUNT : ACCOUNT_WITH_INTEREST; NOW : FREE_CHECKING_ACCOUNT := FREE_CHECKING_ACCOUNT(ACCOUNT, MINIMUM_BALANCE => 0.00, TRANSACTIONS => 0); The Ada 9X rules for conversion between types in a class are consistent with the semantics of inherited operations in Ada 83, and define the semantics of inherited operations in Ada 9X. Calling such an operation is equivalent to calling the parent's corresponding operation with a conversion of the actual to the parent type (see RM 3.4(14), MS-6.4.1). Thus, inherited operations ``ignore'' extensions. User-defined classes may be employed to parameterize generic units. A new kind of generic formal, a generic formal derived type, may be used. This kind of formal is matched by any type derived from the generic formal's specified parent type (see 4.2.3.6). For each type declared in the usual fashion -- whether language-defined or user-defined -- there is an associated class-wide type that is implicitly declared immediately following it. For a type T, the class-wide type is denoted by the attribute T'CLASS. In Ada 9X, ordinary types are referred to as specific types to distinguish them from class-wide types (MS-3.3). For a type T, the set of values of the class-wide type T'CLASS is the union of the sets of values of T and all of its derivatives. There are two cases to consider: when T is untagged and when T is tagged. If a type T is untagged, then the values of T'CLASS are essentially a copy of those of T. Operations on T may be called with values of T'CLASS. Operations defined for T'CLASS may be called with values of any type within the class. In Ada 83, universal_integer and universal_real can be thought of as class-wide types for the INTEGER and REAL classes, respectively. In effect, Ada 9X makes these capabilities available for user-defined classes. If a type T is tagged, the set of values of T'CLASS is the discriminated union of the sets of values of T's derivatives with a type tag discriminating values of one derivative from those of another. This type tag, associated with each value of a tagged class-wide type, is the basis for adding run-time polymorphism in Ada 9X. Whether tagged or untagged, class-wide types have no primitive operations of their own. However, explicit operations may be declared for such types, using the attribute (e.g., T'CLASS) as a type mark. Such operations are ``class-wide'' and are available for objects of any type within the class. Examples of Class-Wide Operations: function SIZE_IN_BYTES(ANY_FILE : FILE'CLASS) return NATURAL; function GET_FILE_FROM_USER return FILE'CLASS; A type is tagged if its declaration contains the reserved word tagged, or it is derived from a tagged type. Tagged types permit extension (as shown above). When a type T is tagged, the associated class-wide type is ``dynamic'' in the sense of [Abadi 91]. The values of the class-wide type can be thought of as pairs consisting of: a tag a type descriptor ranging over the types that are members of the class; and a value taken from the type with the given tag. Such values are strongly typed, consistent with the philosophy of Ada. However, only the class, and not necessarily the type within that class, may be known statically. When a primitive operation of a tagged type is called with an operand of the class-wide type, the operation to be executed is selected at run time based on the type tag of the operand. This run-time selection is called dispatching, so primitive operations of tagged types are called dispatching operations. Dispatching provides Ada programmers with a natural form of run-time polymorphism within classes of related (derived) types. This variety of polymorphism is known as ``inclusion polymorphism'' [Cardelli 85]. Example: type FILE is tagged private; procedure VIEW(F : FILE); -- Type file F to screen type DIRECTORY is new FILE with private; procedure VIEW(D : DIRECTORY); -- List directory D type ADA_FILE is new FILE with private; procedure VIEW(A : ADA_FILE); -- Open A with Ada-sensitive editor type ADA_LIBRARY is new DIRECTORY with private; procedure VIEW(L : ADA_LIBRARY); -- List L's library units and their status declare A_FILE : FILE'CLASS := GET_FILE_FROM_USER; begin VIEW(A_FILE); end; The example presents a user-defined class of FILE types. Type FILE is tagged; hence, the primitive operation VIEW is dispatching. VIEW is overridden for each type in the class, to provide a unique behavior for each type of FILE. When VIEW is called with a parameter that is of type FILE'CLASS, the tag will be used to determine the actual type within the class, and the call will dispatch to the VIEW procedure for that type. 4.2.3. Benefits and Intended Use This section presents some of the ways in which Ada 9X's object-oriented programming features may be used and combined with other facilities to address a number of programming paradigms. Ada 9X's facilities for object-oriented programming were designed to the following criteria: Compatibility Any solution should be upward compatible with Ada 83. Legal Ada 83 programs should remain legal. Ideally, existing ADTs should be reusable with newly developed ones -- the new facilities should not be so radically different from mechanisms of Ada 83 that existing ADTs must be rebuilt before being reused. Tagged type extension and class-wide types are built upon Ada 83's model of derived types. Their use is optional, and their presence in the language does not effect Ada 83 programs. Existing ADTs may be combined in some ways with new object-oriented abstractions without modification (as in Ada 83). In other cases, it may be sufficient to add a tagged to a type declaration, or make other simple modifications (e.g, changing an access type declaration to designate a class-wide type). Of course, to fully exploit these facilities, it will be necessary to take them into account during the design process. Consistency The solution should be conceptually consistent with Ada's programming model(s). Programmers' intuitions about objects, types, subprograms, generic units, should be preserved. It is a goal of the Mapping to provide these new capabilities in the context of a unified programming model: including types, operations and generic units. Class-wide programming is intended to generalize the classes developed in Ada 83 to user-defined classes, and class-wide types generalize Ada 83's universal types. Efficiency The solution should offer efficient performance for users of the facility with, ideally, no distributed overhead for non-users. The introduction of tagged types, and a distinct class-wide type associated with each specific type as the mechanism for dispatch, makes run-time polymorphism optional to programmers (in contrast to languages like Smalltalk), in two senses. - Programmers can choose whether or not to use object-oriented programming, by employing tagged and class-wide types. Types without tags incur no space or time overhead. Only class-wide types allow for class-wide type matching. - Dispatching occurs only on primitive operations of tagged types. The target of the dispatch may be determined at compile-time, when the operation is called with a value of a specific type (within the class), or if the tag is statically determinable. Implementability The solution should be readily implementable within current compiler technology, and provide opportunities for optimizations. - Dispatching may be implemented as an indirect jump through a table of subprograms indexed by the primitive operations. This compares favorably with method look-up in many object-oriented languages, and with the alternative in Ada 83: variant records and case statements, with their attendant variant checks, both in implementability and run-time efficiency. - The User/Implementor teams have demonstrated the feasibility of implementing these features. 4.2.3.1. Variant Programming A predominant use of the object-oriented programming paradigm is variant programming (as noted above). The following example suggests how the use of tagged type extension could simplify the construction and maintenance of a complex system that might have otherwise been built with Ada 83 variant records. Variant Programming in Ada 83: with CALENDAR; package ALERT_SYSTEM is type PRIORITY is (LOW, MEDIUM, HIGH); type DEVICE is (TELETYPE, CONSOLE, BIG_SCREEN); type ALERT (P : PRIORITY) is record TIME_OF_ARRIVAL : CALENDAR.TIME; MESSAGE : TEXT; case P is when LOW => null; when MEDIUM | HIGH => ALERT_ACTION_OFFICER : PERSON; case P is when LOW | MEDIUM => null; when HIGH => RING_ALARM_AT : CALENDAR.TIME; end case; end case; end record; procedure DISPLAY(A : in ALERT; ON : in DEVICE); procedure HANDLE(A : in out ALERT); procedure LOG(A : in ALERT); procedure SET_ALARM(A : in ALERT); end ALERT_SYSTEM; A record type with variant part is used to model alerts. The actual structure and processing depends on their priority, which is used as a discriminant. There are several problems with this approach: wherever code exists to handle or otherwise manipulate alerts, case statements must be used to determine the actual subtype of the alert prior to its processing. Processing A Variant Record (Ada 83): procedure HANDLE(A: in out ALERT) is begin A.TIME_OF_ARRIVAL := CALENDAR.CLOCK; LOG(A); DISPLAY(A, TELETYPE); case A.P is when LOW => null; -- do nothing special when MEDIUM | HIGH => A.ALERT_ACTION_OFFICER := ASSIGN_VOLUNTEER; DISPLAY(A, CONSOLE); case A.P is when LOW | MEDIUM => null; when HIGH => DISPLAY(A, BIG_SCREEN); SET_ALARM(A); end case; end case; end HANDLE; Using variant records can be cumbersome and error-prone [Wirth 88]. The code above is structurally complex, due to the relationship between the medium and high priority variants of the record. A high priority alert is just like a medium priority alert in that each has an associated action officer, but with an additional component (RING_ALARM_AT). These logical similarities must also be repeated in the operations on ALERT. The variant record approach is also fragile in maintenance. If the programmer must modify the system to support a new priority level (e.g., for emergency alerts), then type PRIORITY must be modified, as must type ALERT, as well as any subprograms that handle alerts, even if these operations do not require any additional logic for the emergency situation. Such a change will require the recompilation of all such units -- and cannot even be restricted to the package body of ALERT_SYSTEM. Of course, in such a small system, the effects of such a modification are manageable, however, in a larger system, such as a command and control or mail system dealing with a large number of structured message forms, or a compiler or interpreter dealing with a large number of node types, these effects become a significant problem. With Ada 9X, the programmer can use tagged type extension and subprogram dispatch to simplify the system, handling each variant as a derived type extension, completely eliminating variant records and case statements. Processing for each kind of alert is localized to a type and dispatch will insure that the proper operation is called for each instance. Variant Programming in Ada 9X: with CALENDAR; package NEW_ALERT_SYSTEM is type DEVICE is (TELETYPE, CONSOLE, BIG_SCREEN); type ALERT is tagged record TIME_OF_ARRIVAL: CALENDAR.TIME; MESSAGE: TEXT; end record; procedure DISPLAY(A : in ALERT; ON: in DEVICE); procedure HANDLE(A : in out ALERT); procedure LOG(A : in ALERT); procedure SET_ALARM(A : in ALERT); type LOW_ALERT is new ALERT; type MEDIUM_ALERT is new ALERT with record ACTION_OFFICER: PERSON; end record; procedure HANDLE(MA: in out MEDIUM_ALERT); type HIGH_ALERT is new MEDIUM_ALERT with record RING_ALARM_AT: CALENDAR.TIME; end record; procedure HANDLE(HA: in out HIGH_ALERT); end NEW_ALERT_SYSTEM; The variant record type has been replaced with an ALERT type and three types derived from it. The enumeration type PRIORITY is gone. (We have retained the type DEVICE, but see 4.2.3.4.) Ada 9X's relaxation of restrictions on derivation in the visible part of a package (see MS-3.3.3) allow us to encapsulate these four closely related types in a single package (cf. RM 3.4(15)). Type LOW_ALERT is exactly a copy of ALERT (and could be dispensed with although perhaps in the future additional capabilities will be added to it, so we maintain equivalence with the original variant record type as indexed by PRIORITY). MEDIUM_ALERT extends ALERT and has its own HANDLE procedure. HIGH_ALERT extends MEDIUM_ALERT and also overrides procedure HANDLE. Instead of the single procedure HANDLE embodying a case statement as in the Ada 83 solution, the Ada 9X solution distributes the logic for handling alerts to each specific alert type, without redundancy. Variant Programming (Ada 9X): package body NEW_ALERT_SYSTEM is procedure HANDLE(A : in out ALERT) is begin A.TIME_OF_ARRIVAL := CALENDAR.CLOCK; LOG(A); DISPLAY(A, TELETYPE); end HANDLE; procedure HANDLE(MA: in out MEDIUM_ALERT) is begin HANDLE(ALERT(MA)); -- conversion (no dispatch) MA.ACTION_OFFICER := ASSIGN_VOLUNTEER; DISPLAY(MA, CONSOLE); end HANDLE; procedure HANDLE(HA: in out HIGH_ALERT) is begin HANDLE(MEDIUM_ALERT(HA)); -- conversion (no dispatch) DISPLAY(HA, BIG_SCREEN); SET_ALARM(HA); end HANDLE; procedure DISPLAY(A : in ALERT; ON: in DEVICE) is separate; procedure LOG(A : in ALERT) is separate; procedure SET_ALARM(A : in ALERT) is separate; end NEW_ALERT_SYSTEM; Each body for HANDLE encloses just the code relevant to the type, and delegates additional processing to an ancestor via an explicit type conversion. This is more flexible than the super pseudo-variable of Smalltalk-80 while retaining Ada's static type checking. In Ada 9X, view conversions to a tagged class-wide type preserve the tag of the object to permit repeated dispatch within the class determined by the target type (see 4.2.3.4). If a new kind of alert for emergency situations must be added, it may be done without disturbance or recompilation of the existing system code as a separate package. Adding A New Variant: with NEW_ALERT_SYSTEM; package EMERGENCY_ALERT_SYSTEM is type EMERGENCY_ALERT is new NEW_ALERT_SYSTEM.ALERT; procedure HANDLE(EA : in out EMERGENCY_ALERT); procedure DISPLAY(EA : in EMERGENCY_ALERT; ON : in NEW_ALERT_SYSTEM.DEVICE); procedure LOG(EA : in EMERGENCY_ALERT); end EMERGENCY_ALERT_SYSTEM; 4.2.3.2. Heterogeneous Data Structures The previous example developed a class of alert types, and type-specific ways of processing them. Any application built for processing alerts must be able to handle these types in a uniform manner, such as for input/output, logging, etc. However, objects of ALERT'CLASS are of unknown, varying size, due to the possibility of extensions. For this reason, Ada 9X treats them as unconstrained (MS-3.4.2), analogous to unconstrained array types (e.g., STRING) in Ada 83. When a class-wide type, T'CLASS, appears as the designated type in an access type declaration, the resulting type may designate any object within the class rooted at T. Using such class-wide access types will be a common idiom of object-oriented programming in Ada 9X. One could continue the previous example as follows. -- insert in package NEW_ALERT_SYSTEM: type ALERT_PTR is access ALERT'CLASS; . . . BUFFER : array (ALERT_INDEX) of ALERT_PTR; . . . procedure PROCESS_ALERTS is NEXT : ALERT_PTR := null; begin while OPERATIONAL loop ALERT_IO.GET(NEXT); -- some kind of I/O ADD_TO_BUFFER(NEXT); HANDLE(NEXT.all); -- dispatching on tag end loop; end PROCESS_ALERTS; Tagged type extension may also be used to implement heterogeneous data structures that have a common interface, for example a stack or list, but whose values are of various types. The root type of the class defines the basic operations for constructing and accessing the structure. Its derivatives extend the root type with the types of interest (see 3.2.3). 4.2.3.3. Multiple Implementations A variation on the previous usage idiom is to develop a family of diverse implementations of a single abstraction, such as a family of list types [LaLonde 89], matrices (dense or sparse), or set types, as in the next example. Abstract Specification of Sets: -- Given: subtype SET_ELEMENT is NATURAL; package ABSTRACT_SETS is type SET is tagged private; -- Empty set function EMPTY return SET is <>; -- Build set with 1 element function UNIT(ELEMENT : SET_ELEMENT) return SET is <>; -- Union two sets function UNION(LEFT, RIGHT : SET) return SET is <>; -- Intersect two sets function INTERSECTION(LEFT, RIGHT : SET) return SET is <>; -- Remove an element from a set procedure TAKE(FROM : in out SET; ELEMENT : out SET_ELEMENT) is <>; -- Return largest possible element of set function MAX_ELEMENT(ANY : SET) return SET_ELEMENT is <>; ELEMENT_TOO_LARGE : exception; private type SET is tagged record null; end record; end ABSTRACT_SETS; The package provides an abstract specification of sets. The SET type definition is a tagged private type, whose full type declaration is a null record. It defines a set of primitive operations on SET that are abstract subprograms (MS-6.1). Abstract subprograms do not have bodies and may not be called directly. However, as primitive operations, they are inherited. Derivatives of SET must override these abstract operations to provide their own implementations. Note that the function MAX_ELEMENT must have a ``dummy parameter'' of type SET, so that it will be treated as a primitive operation of the type. Derivatives of SET would extend the root type with a component providing the desired data representation, and implement the primitive operations for that representation. For example, one might build an implementation upon bit vectors: Bit Vector Extension of Set: with ABSTRACT_SETS; package BIT_VECTOR_SETS is type BIT_SET is new ABSTRACT_SETS.SET with private; -- Override the abstract operations -- (implementations could use boolean array operations) function EMPTY return BIT_SET; function UNIT(ELEMENT : SET_ELEMENT) return BIT_SET; function UNION(LEFT, RIGHT : BIT_SET) return BIT_SET; function INTERSECTION(LEFT, RIGHT : BIT_SET) return BIT_SET; procedure TAKE(FROM : in out BIT_SET; ELEMENT : out SET_ELEMENT); function MAX_ELEMENT(ANY : BIT_SET) return SET_ELEMENT; private BIT_SET_SIZE : constant := 64; type BIT_VECTOR is array(SET_ELEMENT range 0..BIT_SET_SIZE-1) of BOOLEAN; pragma PACK(BIT_VECTOR); type BIT_SET is new ABSTRACT_SETS.SET with record DATA : BIT_VECTOR; end record; end BIT_VECTOR_SETS; 4.2.3.4. Multiple Dispatch There are often situations where one would like ``multiple dispatch'' either within a class, or between two or more classes. Ingalls cites a number of canonical examples such as displaying various kinds of graphical objects on varying kinds of displays, event types and handlers; unification and pattern matching [Ingalls 86], and suggests a solution for Smalltalk-80 that is more modular than a single dispatch on one parameter, followed by a case statement on the dynamic type of a second parameter. Multiple dispatch is possible in Ada 9X via class-wide types. For instance, in the example above on alerts, the type DEVICE might not be represented as a simple enumeration, but instead a record type, with components representing various aspects of the device. A class of device types could be constructed using tagged types and type extension. Each kind of device must implement an OUTPUT operation that each kind of alert will use to implement its DISPLAY operation. In order to call the appropriate OUTPUT procedure two dispatching operations are involved. First, the type of the alert parameter controls the dispatch to the DISPLAY procedure, and then within that procedure a dispatch on the DEVICE parameter will select the appropriate OUTPUT operation for the device being used as a display. This double dispatching can be accommodated in Ada 9X by making DISPLAY a class-wide operation of the device class. The specification of the DISPLAY procedure for ALERT becomes: procedure DISPLAY(A : ALERT; ON : DEVICE'CLASS); Within each DISPLAY procedure, a call to OUTPUT, with parameter ON will dispatch to the appropriate operation for the DEVICE. 4.2.3.5. Encapsulation and Extension In Ada, packages and private types are the mechanisms for encapsulation. These mechanisms are extended in Ada 9X to support tagged type extension. A (limited) private type may be tagged, if its full type declaration is a record type. A tagged private type may be extended with either a record or private extension part. In combinations with child library units (see MS-10.1), tagged type extension may be used to provide ``dual'' interfaces, one view to support application clients of an abstraction, and the other view to support developers needing to extend the abstraction (see 4.3). 4.2.3.6. Class-Wide Programming and Generic Units Class-wide programming and type extension, in combination with generic units, provides many useful facilities. Generic units may be instantiated with class-wide types, yielding class-wide operations. For example, instantiating package TEXT_IO.INTEGER_IO with a class-wide type produces an input/output package usable with any type in that class. This encourages the user to use distinct types for particular needs without the excessive burden of instantiating INTEGER_I/O for each such type. Instantiating a generic unit with a class-wide type: type INT32 is range -2**31..2**31-1; -- Users derive from this to get a portable 32 bit integer -- This IO package can be used with any type derived -- from INT32. package INT32_IO is new TEXT_IO.INTEGER_IO(NUM => INT32'CLASS); Generic units may be parameterized by user-defined classes, allowing abstractions to be built around such classes. In this example, ANY_ACCOUNT will be matched by any type derived from ACCOUNT_WITH_INTEREST. Within the template, the primitive operations of ACCOUNT_WITH_INTEREST are available. generic type ACCOUNT_TYPE(<>) is new ACCOUNT_WITH_INTEREST; package SET_OF_ACCOUNTS is procedure ADD_NEW_ACCOUNT(A : in ACCOUNT_TYPE); procedure REMOVE_ACCOUNT(A : in ACCOUNT_TYPE); function BALANCE_OF_ACCOUNTS return MONEY; . . . -- other operations (e.g. an iterator) end SET_OF_ACCOUNTS; This generic package could be instantiated with a specific derivative of ACCOUNT_WITH_INTEREST, in which case it would be a homogeneous set of such accounts. Alternatively, the generic could be instantiated with a class-wide type like ACCOUNT_WITH_INTEREST'CLASS, in which case it would allow a heterogeneous set of accounts. The notation (<>) specifies that the actual account type may have any number of discriminants, or be a class-wide type (see MS-12.3.6). A generic unit may extend a tagged type, adding components and operations. The extended types declared within such generic units inherit all the properties of the original type, and possess all the new properties defined by the generic template. Such generic units act as ``mixin'' classes and provide one of multiple inheritance (see 4.2.4.4). 4.2.4. Relationship to Previous Work 4.2.4.1. Data Abstraction Data abstraction is the foundation for object-oriented programming. Ada 83 provides the basis for supporting OOP with its extensive facilities for user-defined abstract data types. Types may be defined, composed and parameterized by values of other types, and encapsulated. The approach taken by Ada 83 provides several predefined type classes, and constructors for defining new types within these classes (see 2.1.2). The Ada 83 facilities support a safe, high-level, programming style and provide a basis on which to build new systems from pre-existing components. While reusing such components ``as is'' can be useful, for the development of large software systems it is often critical to be able to reuse such components with some amount of variation or adaptation to the specific requirements of the new system to fully facilitate reuse [Cohen 90]. The recognition of the need to improve Ada's facilities in this area led to the requirements cited above (see 4.2.1). The facilities for user control of user-defined abstractions (i.e., abstract data types) are somewhat incomplete in Ada 83. Key operations on ADTs cannot be defined by the programmer, particularly in the face of abstraction (encapsulation as a private type, or when parameterized as a generic formal type). These key operations include: type initialization and finalization, and user-defined equality. Ada 9X allows an abstract data type to have user-defined initialization and finalization (MS-G, Systems Programming Annex) and generalizes user-defined equality/inequality for any type (MS-6.7). Initialization and finalization are defined for controlled types. ``Finalization'' permits the programmer control over a type's objects at the end of their lifetimes (such as prior to deallocation, on scope exit, transfer of control), to ensure their final state and perhaps to release, reclaim, or otherwise control, shared resources associated with the objects. Although Ada 83 has a detailed semantics of elaboration, the language says little about what happens to objects at the end of their lifetimes; finalization on scope exit is implicit for task types and access type collections in the language. In Ada 9X, users may define their own finalization procedures on a per type basis. Ada 83 provided support for variant and class-wide programming, as defined above, in the form of derived types, subtypes, packages and private types, and generic units. Each of these has its limitations, however. A derived type is a copy of its parent type, inheriting the parent's structure and operations. However, this derived type may not be readily extended with additional structure. Subtypes are ``refinements'' of their base type with additional constraints; but such constraints are limited to range, index, accuracy or discriminant constraints, narrowing their applicability rather than extending it. Packages and private types are the principal means of creating and exporting an abstraction (ADT), but such an abstraction has a single interface to clients: the visible part, which exports one or more types and operations on those types. A developer intending to extend an ADT must use the same interface as the application programmer or must modify the original package, thereby disrupting other clients. The goals of encapsulation of an ADT for its end users versus potential reusers can be in conflict. Private types have therefore been criticized as ``too private'' because reusers of an ADT often need access to internal properties of that abstraction, (i.e., the private part) [Bardin 89]. In Ada 83, this necessitates that the developer ``break'' the abstraction provided in the existing package, either: - by copying the code from it into a new package (breaking any relation between the existing and the new abstraction); or, - by modifying the existing package to make available whatever data structures or operations are needed to reuse it; or, - by bundling the two abstractions together (i.e., by putting them into a single package, and thereby forcing a relationship between abstractions that might not be appropriate for other users); or, - by manually importing and re-exporting each necessary element of the old abstraction into the new one. In many cases, recompilation of the original abstraction and all of its clients is required. Generic units provide a means of making import and re-export more automatic, but this is only possible when it has been anticipated by the original developers of the components. In addition, limitations on the expressivity of generic formal parameters also complicate this style of use (for example, the absence of generic formal record types). Some of these have been addressed by Ada 9X (see MS-12.1.2, 12.1.4). Ada 83 already supports several forms of static polymorphism: generic formal types, subprogram overloading, and implicit conversion of class-wide (real and integer) literals. Ada 9X adds class-wide operations as well run-time polymorphism within a class of related types as an option for programmers, while retaining Ada's generally static type model. 4.2.4.2. Inheritance Mechanisms As described above, inheritance is a mechanism for deriving one abstraction from another, specifying only the difference between the new (derived) and old (parent). Inheritance establishes a relationship between these abstractions, usually a dependence of the derived class on the parent with the benefit of eliminating the need to reprogram/rewrite each new abstraction from scratch. If DERIVED inherits from PARENT, then DERIVED is related to PARENT by having all the properties of PARENT, possibly modified as follows: - DERIVED may add properties to those it inherits from PARENT - DERIVED may replace properties inherited from PARENT with ones of its own - DERIVED may rename properties of PARENT for its own purposes - DERIVED may remove properties of PARENT Object-oriented programming originated with Simula [Birtwistle 73]. Simula was designed to be an almost upward-compatible extension of Algol-60, inspired by the application domain of simulation, although it is really a general-purpose programming language. The key insights from simulation were that it is useful to think of a complex simulation as being organized around a collection of autonomous, interacting objects, and that the construction of such simulations could be facilitated by abstracting this notion of object into a language construct. Simula introduced the notion of a class as an abstraction mechanism over objects. A class is a template for creating objects with a common data structure and operations on that data structure. These operations determine the possible behavior of the objects of the class. Operations may be sensitive to the current state of the object, and may update that state by changing the values of the data structure. A Simula class definition specifies a data structure for the class, the operations on that data, and a body used to initialize objects of the class upon their creation, like the sequence of statements in a package body. The data definition and procedure declarations constitute the class's interface to programmers. The Simula class is somewhere between a data type and a module. Instances of the class may be declared, assigned to variables, and passed as parameters, like values of a typical data type. Simula introduced a means to define new classes from old ones; a class could ``inherit'' from another class, deriving its structure and operations from that ``parent''. The new class could augment or override its inheritance, adding new data and new operations, or replacing one or more of its operations. Data could not be removed. Smalltalk [Goldberg 83] was influenced by Simula's notion of class and subclassing. While Simula was a compiled language, Smalltalk was interpreted. It was originally intended as an interactive, systems programming language for Alan Kay's Dynabook project. Smalltalk introduced the ``message-passing'' style of invoking operations. A message is a request to an object to invoke an operation. The set of messages that an object recognizes and is capable of responding to is called its ``protocol'' and is determined by the class of the object. When an object is sent a message, a search begins in that object's class for a method (operation definition) corresponding to the message. If not found, the search continues in the parent class (superclass), this continues upward in the class hierarchy until either an appropriate method is found or the root of the hierarchy is reached without success, in which case an error is signaled. The historical fact that some early object-oriented languages were interpreted has contributed to the impression that their mechanisms are necessarily too inefficient for real-time or production use. Many object-oriented languages (including Simula) also use implicit reference semantics, raising the issue of run-time storage management. It was these efficiency considerations that apparently prevented Ada 83 from providing inheritance and polymorphism, given Ada's overriding concerns with run-time efficiency, and type safety [Brosgol 89]. More recently, there have been a number of languages developed that support object-oriented programming in a relatively safe, compiled, and efficient style, including Trellis/Owl [Schaffert 86], Eiffel [Meyer 88], and C++ [Ellis 90]. 4.2.4.3. Run-Time Dispatch When a dispatching operation is called, a determination must be made, usually at run time, what actual operation must be invoked. In Smalltalk-80, method invocations have the form: Receiver methodName argsToMethod This syntax simplifies dispatch; the dispatch is determined solely by the class of the receiver of the message. Eiffel and C++ also use the ``distinguished receiver'' approach. In languages where a function or procedure call syntax is permitted, and where more than one argument of the call may be of the class, the situation is more complex. Trellis/Owl [Schaffert 86]) follows the Smalltalk-80 tradition and arbitrarily designates the first parameter of the call as determining the dispatch. Other possible schemes include: 1. All parameters within the class must share the same type tag. 2. The programmer must select a parameter as the controlling one, as a part of the declaration of the parameter's mode. 3. All parameters must share the same code for the operation. 4. The most specific type within the class (``nearest ancestor'') applicable to all of the parameters is used. 5. The most general type within the class (``furthest ancestor'' i.e., the root) applicable to all of the parameters is used. Ada 9X has adopted (1). This is the logical choice, given that the dispatching operations of a type are the primitive operations of that type and are derived from those of the root type with systematic replacement -- most operations will be written in terms of a single specific type. 4.2.4.4. Multiple Inheritance Some languages permit a derived type, or class, to have more than one parent. These languages are said to support ``multiple inheritance.'' Multiple inheritance is a second-generation object-oriented programming mechanism. It originated in MIT's FLAVORS extension to LISP; a precursor to the Common Lisp Object System. Most uses of multiple inheritance fall into one of three idioms. The first is, to quote N. Guimaraes of AT&T, ``to combine two classes, one that defines the protocol of the component, and another that provides an implementation'' [Guimaraes 91]. In languages such as Eiffel and C++, where classes are the only form of module, inheritance is the most common mechanism for combining abstractions. For instance, a class BOUNDED_STACK[T], could be constructed by inheriting from an abstract class STACK[T] and a second class ARRAY[T]. Class ARRAY[T] would then be used to implement the abstract operations not defined by class STACK[T]. The programmer must specify the implementation of each such operation, and ideally, the array operations should also be hidden from users of BOUNDED_STACK[T]. The effect of this idiom of multiple inheritance can be achieved in Ada 83 through type composition -- inheritance is not required. In Ada, one may implement one type in terms of another, and hide that implementation as a private type. package BOUNDED is type BOUNDED_STACK(SIZE : NATURAL := 0) is private; procedure PUSH(S : in out BOUNDED_STACK; ELEMENT : T); procedure POP(S : in out BOUNDED_STACK); function TOP(S : BOUNDED_STACK) return T; private type BOUNDED_STACK(SIZE : NATURAL := 0) is array (1..SIZE) of T; end BOUNDED; Using the idiom of section 4.2.3.3, one could derive from a tagged abstract type STACK, and implement bounded stacks as arrays. In either case, the operations on BOUNDED_STACK must be explicitly declared, whether being defined or overridden. A second idiomatic use of multiple inheritance can be termed mixin inheritance. In mixin inheritance, one of the parent classes cannot have instances of its own and exists only to provide a set of properties for classes inheriting from it. Typically, this abstract, mixin class has been isolated solely for the purpose of combining with other classes. Ada 9X can provide mixin inheritance using tagged type extension (single inheritance) and generic units. The generic template defines the mixin. A generic formal parameter specifies the parent. Here, we present a generic package that adds the property of having multiple versions to any tagged record or private type. Example of ``mixin'' inheritance: with OM; -- Object Manager provide unique object IDs with VM; -- Version Manager provides version control generic type PARENT is tagged private; package VERSIONED is -- A versioned object has an ID, which identifies -- the set of versions of that object, plus a version -- number that, combined with the ID, identifies an -- object uniquely. type VERSIONED_OBJECT is new PARENT with private; -- Given an object, return a new version of that object. procedure CREATE_NEW_VERSION(O : in VERSIONED_OBJECT NEW_O : out VERSIONED_OBJECT); -- Given an object and a version number, return that -- version of the object. procedure GET_VERSION( ID_FROM : in VERSIONED_OBJECT; VERSION : in VM.VERSION_NUMBER; OBJECT : out VERSIONED_OBJECT); private type VERSIONED_OBJECT is new PARENT with record ID : OM.OBJECT_ID := OM.UNIQUE_ID ; VERSION : VM.VERSION_NUMBER := VM.INITIAL_VERSION; end record; end VERSIONED; Finally, there are uses of multiple inheritance where the derived type or class is truly a derivative of more than one parent and clients of that type want to ``view it'' as any of its parents. This may be accomplished in Ada 9X using access discriminants (MS-3.6.1). Multiple Views using Access Discriminants: package WINDOWS is type WINDOW is tagged private; -- Assume operations like: -- INITIALIZE, SHOW, HIDE, KILL, etc. procedure SHOW(WIN : in out WINDOW); . . . end WINDOWS; package TREES is type TREE is tagged private; type ANY_TREE is access all TREE'CLASS; -- Assume class-wide operations such as -- CREATE, GET_PARENT, INSERT_CHILD, ... -- defined on any TREE. For example: function GET_PARENT(T : access TREE) return ANY_TREE; . . . end TREES; with WINDOWS; use WINDOWS; with TREES; use TREES; package HYPERTEXT_SYSTEM is type NODE is new WINDOW with private; type STRUCTURE_VIEW(SELF : access NODE'CLASS) is new TREE; procedure ADD_NODE(N : access NODE); -- Navigation operations: procedure UP(N : access NODE); procedure NEXT(N : access NODE); procedure PREVIOUS(N : access NODE); . . . private type NODE is new WINDOW with record HYPER : STRUCTURE_VIEW(NODE'ACCESS); end record; end HYPERTEXT_SYSTEM; Here we have assumed two preexisting type classes, of windows, and trees. We want to combine them to build a ``hypertext'' system consisting of a hierarchically structured collection of windows which we can manipulate as ordinary windows as well as navigate through, based on a superimposed tree structure. To do this, we have declared a hypertext NODE type as an extension of a WINDOW, and a STRUCTURE_VIEW derived from TREE. The NODE type is then completed by extension with view type. When the value of an access discriminant within in an extension part refers to the type being declared (in this case NODE), the attribute designates the ``enclosing object'' (see MS-3.8.2). Operations on NODE may act on the inherited ``window'' view and/or the ``structure'' view of an object. Because NODE is derived from WINDOW, its primitive operations are available for NODE. The operations on TREE are also available via component selection. For a NODE, e.g., ROOT_HYPER_NODE, ROOT_HYPER_NODE.HYPER may be manipulated as a TREE. Via its access discriminant, operations can refer back to the enclosing object as ROOT_HYPER_NODE.HYPER.SELF. So, to implement a ``navigational operation'' like UP, we first treat the NODE as an element in the TREE to determine its parent. (Since GET_PARENT has a result of a class-wide access type, we must convert the result to STRUCTURE_VIEW.) We then obtain the window view of that entity using the access discriminant (SELF) as argument to a window display operation (SHOW): procedure UP(N : access NODE) is begin SHOW(STRUCTURE_VIEW(GET_PARENT(N.HYPER)).SELF); end UP; Given the need to balance the benefits of true multiple inheritance with the complexity of the revised language, the potential for distributed overhead caused by multiple inheritance, and the scope of the revision, we chose not to include a multiple inheritance mechanism in Ada 9X. A single inheritance mechanism, based on derived types, is constructed fairly easily, without changes to Ada's conceptual model. As the previous discussion suggests, the three idioms of multiple inheritance may be obtained in Ada 9X using tagged type extension, access discriminants, and generic units together with Ada's powerful type composition mechanism. 4.3. Child Library Units Ada 9X introduces child library units which allow a degree of hierarchical organization within a program library. This organization is useful to reduce recompilation, to provide language support for subsystems (collections of dependent abstractions), to allow multiple views of an abstraction to diverse clients; and to enable an incremental style of programming. 4.3.1. Purpose and Requirements Satisfied Child library units address the following Ada 9X requirements: Study Topic 4.3-A(1) -- Reducing the Need for Recompilation Study Topic 4.3-B(1) -- Programming by Specialization/Extension Study Topic 4.3-C(1) -- Enhanced Library Support Separate compilation of program unit specifications and bodies is a powerful facility in Ada. It supports good software engineering practice by separating the abstract interface of the unit from its implementation. Clients of the program unit need only know about its specification -- changes to the body do not effect such clients, and do not necessitate a client's recompilation. Further control over unit dependencies is provided in Ada 83 by subunits, which permit the separate compilation of nested bodies. Ada 83 also provides this separation of interface from implementation for types, via private types. Private types have an interface in the visible part of the package in which they are declared and an implementation in the private part of that package. Declarations in the private part are only visible within that private part and in the package body. For very complex type definitions, the relationship between private types and packages introduces an unnecessary coupling between abstractions. Consider, for example, a system that implements two private types, FILE_DESCRIPTOR and EXECUTABLE_FILE_DESCRIPTOR. The first supports general file operations, and the second supports special file operations for executable files. For example, EXECUTABLE_FILE_DESCRIPTOR might have a write operation that uses scattering writes to write out an entire executable file quickly. If EXECUTABLE_FILE_DESCRIPTOR must have access to FILE_DESCRIPTOR's implementation, perhaps for reasons of performance, then both FILE_DESCRIPTOR and EXECUTABLE_FILE_DESCRIPTOR must be defined in the same package. Clients of either FILE_DESCRIPTOR or EXECUTABLE_FILE_DESCRIPTOR will depend on this package. If the definition of EXECUTABLE_FILE_DESCRIPTOR is changed, all units that depend on the common package must be recompiled, even those that only utilize FILE_DESCRIPTOR. The unnecessary coupling between EXECUTABLE_FILE_DESCRIPTOR and FILE_DESCRIPTOR forces more recompilations than are logically necessary. The following example shows how to alleviate this situation using child library units: package FILE_IO is type FILE_DESCRIPTOR is private; -- Operations on FILE_DESCRIPTOR... private . . . end FILE_IO; Child library unit of parent FILE_IO: package FILE_IO.EXECUTABLE_IO is type EXECUTABLE_FILE_DESCRIPTOR is private; -- Operations on EXECUTABLE_FILE_DESCRIPTOR... private . . . end FILE_IO.EXECUTABLE_IO; As a child unit of package FILE_IO, FILE_IO.EXECUTABLE_IO can use the full declaration of the private type FILE_DESCRIPTOR in the declaration of the private type EXECUTABLE_FILE_DESCRIPTOR. Clients of the package FILE_IO do not require recompilation if a child package changes, and new child units can be added without disturbing existing clients. Another way of looking at the example above is to observe a distinction between the different clients of a package. The traditional clients of a package use the package's visible definitions as their interface. There are other clients that extend the package's abstractions. These extending clients may add functionality to the original abstraction, or export a different interface, or do both. Extending clients will require details of the original package's implementation. Packages that are extending clients are tightly coupled to the original package in terms of implementation, but their logical coupling may be looser -- the extending client may be an alternative to the original for other clients. It should be possible to use either package independently. In Ada 9X, one or more child library units can share access to the declarations of their parent's private part -- to extend their parent's visible interface or provide an alternate view of it. The distinction between packages that extend another package and packages that simply use the definitions of another package gives rise to the notion of subsystems. A set of packages that share a set of private types can be viewed as a subsystem or subassembly. This concept is recognized by many design methodologies [Booch 87] and is supported by several implementations in their program library facilities. Subsystems are a useful tool for structured code reuse -- very large software systems can be designed and built by decomposing the total system into more manageable-sized pieces that are related, but independent, components. Ada 9X's child library units allow subsystems to be modeled directly in the language. Programs may use child library units to implement several different kinds of structures. Some possibilities are: - A child package may be used to create specialized interfaces to, or views of, an abstraction. This allows independent abstractions to be combined into larger subsystems. The bit-vector and list-based sets example in paragraph 4.3.3.2 below illustrates this. - The interface to a system may be organized into subsystems if it is too large to be conveniently implemented as a single package, or if clients do not often need all of the facilities that the package provides. See the discussion of CAIS-A in paragraph 4.3.3.3 below. - A hierarchical organization of library units may be used to permit vendor extensions. The form of such extensions can distinguish clearly by package names which parts are standard, and which parts are vendor extensions. An example of this is presented below in 4.3.3.4. - Child library units may be used to define an extensible, reusable, library of components. Users are encouraged to extend or modify the components by adding new children. An example of this is discussed below in 4.3.3.5. 4.3.2. Brief Description In Ada 9X, a library unit package may have child library units. Child library units have the following properties: - Child units are logically dependent on their parent, as if they were nested following the private part of the parent. Therefore, child units have visibility to their parent's visible and private parts. - Child library units are named like nested units; with an expanded name consisting of a unique simple name and their parent's name as prefix. - A child may be any kind of library unit: a (possibly generic) subprogram or package. - This structure may be iterated -- child units that are packages may themselves have children, yielding a tree-like hierarchical structure, beginning at a root library package. Child library units may be separately compiled. They may also be separately ``withed'' by clients. A client's context clause that names a child unit implicitly names all of the child unit's ancestors as well. Within the private part of FILE_IO.EXECUTABLE_IO, declarations in the private part of FILE_IO are visible. Hence the definition of EXECUTABLE_FILE_DESCRIPTOR can use the full declaration of FILE_DESCRIPTOR. There are two kinds of child units -- private child units and visible child units. Private children are those declared using the reserved word private. private package FILE_IO.SYSTEM_FILE_IO is type SYSTEM_FILE_DESCRIPTOR is private; private . . . end FILE_IO.SYSTEM_FILE_IO; A unit's specification may depend on a private child unit only if the unit is itself private and it is a descendant of the original private child's parent. A unit body may depend on a private child unit if it is a descendant of the private child unit's parent. Package bodies may depend on visible child specifications. Visible child packages are intended to provide facilities that are available outside their parent packages. In the example, FILE_IO.EXECUTABLE_IO is a visible child generally available to clients. Private child packages are intended ``for internal use'' in a larger definition. In the example, FILE_IO.SYSTEM_FILE_IO is meant to be private to the hierarchy of units rooted at FILE_IO. A principal design consideration for child library units was that packages should have as much visibility as possible while not allowing private definitions to become generally visible. Child library units observe the following visibility rules. - A parent unit's visible definitions are visible everywhere in any child unit, whether the unit is visible or private. - A parent unit's private definitions are visible in the private part of a child unit, whether the child unit is visible or private. - A parent unit's private definitions are visible everywhere in a private child unit, since the child package is never visible outside of the parent. - A parent's body is never visible in a child unit. As a consequence of these rules, a child unit cannot export one of a parent unit's private definitions by renaming it in the visible part. 4.3.3. Benefits and Intended Use 4.3.3.1. Avoiding Recompilation The first example could be part of a transaction control system for a database. It shows how to use child library units to structure definitions to reduce recompilation. package TRANSACTION_MGT is type TRANSACTION is limited private; procedure START_TRANSACTION(TRANS : out TRANSACTION); procedure COMPLETE_TRANSACTION(TRANS : in TRANSACTION); procedure ABORT_TRANSACTION(TRANS : in TRANSACTION); private . . . end TRANSACTION_MGT; package TRANSACTION_MGT.AUDITING is type TRANSACTION_RECORD is private; procedure LOG(REC : in TRANSACTION_RECORD); private . . . end TRANSACTION_MGT.AUDITING; In the example, some clients require facilities for controlling transactions. Other clients need to be able to log a record of each transaction for auditing purposes. These facilities are logically separate, but transaction records require intimate knowledge of the full structure of a transaction. The solution with child library units is to make a child library unit to support TRANSACTION_RECORDs. Each unit may be compiled and withed separately. Changes to TRANSACTION_MGT.AUDITING do not require recompilation of the parent, TRANSACTION_MGT. 4.3.3.2. Using Child Packages to Create New Interfaces Child library units can be used to add new interfaces to an existing abstraction. This is useful, for example, when conversion functions are needed between types in independently developed abstractions. Imagine that two packages exist implementing sets, one using bit vectors, and the other linked lists. The bit vector abstraction may not have an iterator in its interface, and hence a function cannot be written to convert from the bit vector set to the linked list set. A child package can be added to the bit vector set package that provides an iterator. A new package could then be written to provide the conversion functions, implemented using the iterator interface. package BIT_VECTOR_SET is type SET is private; function UNION(A, B : SET) return SET; function INTERSECT(A, B : SET) return SET; function IS_IN(E : ELEMENT; S : SET) return BOOLEAN; procedure ADD_TO(E : ELEMENT; S : in out SET); private . . . end BIT_VECTOR_SET; package LIST_SET is type SET is private; function UNION(A, B : SET) return SET; function INTERSECT(A, B : SET) return SET; function IS_IN(E : ELEMENT; S : SET) return BOOLEAN; procedure ADD_TO(E : ELEMENT; S : in out SET); function NEXT(S : SET) return ELEMENT; function MORE(S : SET) return BOOLEAN; private . . . end LIST_SET; package BIT_VECTOR_SET.ITERATOR is type SET_ITERATION is private; function CREATE(S : SET) return SET_ITERATION; function NEXT(S : SET_ITERATION) return ELEMENT; function MORE(S : SET_ITERATION) return BOOLEAN; private . . . end BIT_VECTOR_SET.ITERATOR; with LIST_SET; with BIT_VECTOR_SET; package SET_CONVERSION is function CONVERT(FROM : in LIST_SET.SET) return BIT_VECTOR_SET.SET; function CONVERT(FROM : in BIT_VECTOR_SET.SET) return LIST_SET.SET; end SET_CONVERSION; 4.3.3.3. Using Structured Libraries for Subsystems CAIS-A [DoD 89b] is an interface that provides operating system services in a portable way. The CAIS has a very large specification, with hundreds of packages. The central type, which is manipulated by much of the system, is called NODE_TYPE. It is a limited private type defined in a package called CAIS_DEFINITIONS. It is needed throughout the CAIS, but its implementation should be hidden from CAIS application developers. The implementation uses UNCHECKED_CONVERSION inside packages that manipulate the type. There are a number of subsystems, the largest of which are for manipulating Nodes, Attributes, and I/O. These subsystems also share common types. These common types are implemented using visible types declared in support packages. Only packages in the subsystem are supposed to depend on these support packages. Child library units could be used to restructure these definitions. The type NODE_TYPE might be defined at the root of the hierarchy. Packages that contain types and operations for manipulating Nodes, I/O, and Attributes might be child packages of the root library package. The common types would be private, and the support packages would be child packages of the packages that contain the type definitions. 4.3.3.4. Using Structured Libraries for Portability The Ada binding to POSIX is another system with many packages and many types. Some of these types are system independent, and some are explicitly system defined. When designing portable programs it is useful to know when programs depend on system defined definitions, to eliminate such dependencies where possible, and to contain them when they are necessary. The POSIX-Ada binding attempts to preserve the principle of with list portability: users should be able to determine if a program depends on system defined features by examining the names of the packages in its with clauses. At the same time, the system dependent packages often require visibility on the private types of the standard packages, and in Ada 83 this can only be accomplished by nesting them within the standard packages. Since a nested package never appears in a with clause, the visibility needs of such system dependent packages is in conflict with the principle of with list portability. Child library units offer precisely what is needed to resolve this conflict. 4.3.3.5. Object Oriented and Incremental Programming The previous example demonstrates the relationship between object oriented programming and child library units. An important use of child units is to allow types to be extended while preserving them as private types. This simplified example is based on an X Window System-style toolkit. Every object in the toolkit view of the X Window System is a widget. Widgets are used to implement windows. Some widgets are invisible, but serve to mark out a region of the screen. These widgets may contain other widgets. For example, a widget may contain many labels that display program names or status. with X_DEFS; package XTK is type WIDGET is tagged private; private type WIDGET_ACCESS is access WIDGET'CLASS; type WIDGET is record PARENT : WIDGET_ACCESS; CLASS_NAME: X_DEFS.X_STRING; X, Y: X_DEFS.X_POSITION; WIDTH, HEIGHT: X_DEFS.X_DIMENSION; CONTENT: X_DEFS.X_BITMAP; end record; end XTK; -- The definition of a Label widget -- in a child of XTK. with X_DEFS; package XTK.LABEL is type LABEL is new WIDGET with private; private type LABEL is new WIDGET with record LABEL : X_DEFS.X_STRING; COLOR : X_DEFS.X_COLOR_TYPE; end record; end XTK.LABEL; In the example, a tagged private type is extended in a child library package. Within the private part of the child, the full definition of WIDGET is visible. The child unit, too, declares a private type, which may be similarly extended. In this way, ordinary clients of types within the class rooted at WIDGET use only the visible interface to these private types while extending clients have visibility to the types' full declarations. 4.3.4. Relationship to Previous Work 4.3.4.1. Previous Solutions There are two feasible Ada 83 approaches to the problems addressed in this chapter. 1. A user can choose to make the definitions in packages visible, that eliminates the problem by avoiding private types. 2. The definitions of private types may be replicated exactly in independent packages, and UNCHECKED_CONVERSION used to translate between them. These both have disadvantages. The first approach allows dissemination of implementation information throughout all programs. Aside from the problems associated with allowing ordinary clients access to implementation details, this means that clients are not insulated from the effects of implementation changes. Implementation changes will result in the recompilation of all clients and, possibly modification. The second approach is used in the CAIS-A implementation. It too has disadvantages. First, the replicated type definitions must match exactly, which creates a maintenance problem. Second, the compiler must represent each type definition in the same way, which would require a representation clause. Finally, if the data type is a record, the components may themselves be private types whose definitions may need to be replicated. This propagated need for visibility may cause many or all private type definitions to be replicated in several places. 4.3.4.2. Alternative Approaches Lastly, we discuss some alternative approaches that the Ada 9X Mapping/Revision team considered. Here are three alternatives to the child library units: Voyeurism(The term ``voyeurism'' is due to D. Emery.) Allow packages to request visibility on another package's private declarations via some kind of enhanced context clause (such as with all X; or with private X;) Friends Allow packages to specify exactly which other packages may have enhanced visibility. DAG Inheritance Allow a more general form of hierarchical organization, where packages can, in effect, ``nest'' within several other packages, without an explicit indication in the parent. This is called ``DAG inheritance'', since the dependence graph must be a directed, acyclic graph. The voyeur approach has the unsettling characteristic of making the privacy of types a property of other packages' context clauses, instead of a feature of the package that is declaring the type. This inverts the Ada 83 notion of privacy. With child library units, privacy becomes a feature of package hierarchies, which is a generalization of Ada 83's subunit facility. Although many of the same effects can be achieved with either approach, the mechanisms are fundamentally different, and child library units are consistent with this Ada model. There is also another, more serious problem with with private approaches -- without additional rules, private declarations may be easily re-exported to units that do not have a with private clause for the unit. This could happen, for example, via a renaming declaration or deriving from a private type. package CLOSED is type T is private; private type T is FULL_IMPLEMENTATION; end CLOSED; with private CLOSED; package OPEN is type D is new CLOSED.T; -- from full declaration of T end OPEN; with OPEN; package SURPRISE is type S is new OPEN.D; -- Gets full type declaration of T via D end SURPRISE; The rules for private and visible children avoid this under the Ada 9X approach. The ``friends'' approach is used in C++. This solution was considered by the Ada 9X Mapping/Revision team and was rejected because it does not allow for unplanned extension of a package without requiring recompilation of that package, conflicting with the requirement to reduce recompilation. In the X and CAIS-A examples above we discussed situations where unplanned extension is desirable. It is highly advantageous that Ada 9X support it with this same mechanism. Furthermore, the ``friend'' approach inverts Ada's usual client/server philosophy. In general it is not possible to tell at the point of a particular declaration where that declaration will be used. For example, the declaration of a type does not specify the type's users, a task declaration does not specify the task's callers, a library unit's declaration does not specify which other units depend on it, and a generic unit's declaration does not specify where the generic will be instantiated. Allowing a package's declaration to specify which other units may extend that package is inconsistent with this model. Although on the surface the voyeurism and friend concepts appear simple, the issue of transitivity is problematical. In Ada, context clauses are not transitive. Presumably this would also be the case for voyeur context clauses as well. In that case the meaning of the following program becomes unclear. with private X; package Y is . . . end Y; with private Y; with X; package Q is . . . end Q; Here Q has visibility to Y's private declarations which in turn may refer to X's private declarations. However, Q does not have visibility to X's private declarations through its context clause. Any proposal would have to address in this case whether or not Q has visibility to X. The DAG inheritance approach has characteristics of both child library units and voyeurism. If there is more than one private type involved, it is possible that a tree structured hierarchy cannot provide the exact visibility needed. However, the DAG approach is complex and its ramifications far-reaching. The Ada 9X Mapping/Revision Team decided that such a solution is too ambitious for this revision. 4.4. Data-Oriented Synchronization This section describes a new feature of Ada 9X that supports data-oriented synchronization: protected records. 4.4.1. Purpose and Requirements Satisfied Protected records provide a low-level, lightweight, data-oriented synchronization mechanism. They are specially suited for use in real-time systems. The inclusion of protected records in Ada 9X is motivated by the following requirements: Requirement R5.4-A(1) -- Non-Blocking Communication Requirement R6.3-A(1) -- Interrupt Servicing Requirement R6.3-A(2) -- Interrupt Binding Requirement R5.2-A(1) -- Alternative Scheduling Algorithms Requirement R5.2-A(2) -- Common Real-Time Paradigms The first requirement addresses User Need U5.4-A(1) -- Asynchronous Message Passing. Ada 83's rendezvous provides for synchronous communication between a pair of tasks. A mechanism for asynchronous communication is required. This mechanism must also support non-pairwise communication among tasks. The next two requirements (R6.3-A(1) and R6.3-A(2)) pertain to interrupts. In Ada 83, interrupts are bound to task entries (RM 13.5.1). However, with the current semantics of interrupt entries in Ada 83 (RM 13.5.1), the interrupt-handling performance of most Ada implementations has been too slow and too unpredictable for demanding real-time applications. Since interrupt handling is inherently close to the hardware, users are willing to relax portability requirements for a simple, low-level, efficient mechanism. A second concern is that in Ada 83, interrupt bindings are statically defined by representation clauses. Frequently in real-time systems, external events will be handled differently in different mission phases. Consequently there is a need to change the binding of interrupts to handlers during program execution. Lastly, it is possible in Ada 83 for a lower-priority interrupt to preempt a higher-priority interrupt, which is undesirable in real-time systems. The last two requirements (R5.2-A(1) and R5.2-A(2)) derive from User Need U5.2-A -- User Controlled Scheduling. These requirements address priorities and caller's selection from entry queues. Entry queue selection is sometimes called preference control. Many papers discussing preference control have appeared in the literature [Elrad 88, Wellings 83, Burns 87, Burns 89]. Preference control arises in applications like resource allocation servers, which typically grant satisfiable requests and queue up unsatisfiable requests for later servicing. Wellings, Keefe and Tomlinson [Wellings 83] were unable to find a good way to implement such servers in Ada 83. Finally, tasks themselves are heavy artifacts. By this we mean that tasks offer a lot of capability, and as a result, operations like task creation are relatively expensive, both in time and space. In hard real-time applications, it is prohibitively expensive to schedule a large number of tasks, and to reserve enough storage for them. This makes solutions, which are based on creating many intermediary tasks, unsuitable for these, or any other performance-sensitive, applications. Implementation-defined optimizations, to make tasks ``lighter'', do exist, but they introduce non-portability into the system's design. Note that this section assumes the support of some of the annexes, in addition to the core 9X language. 4.4.2. Brief Description This section briefly describes the protected record feature of Ada 9X (see MS-9 and MS-H, Real-Time Systems Annex). Because protected records are intended to be very efficient, the description method is somewhat implementation-oriented. These are the key features of protected records (see the MS for more details): - A protected record has components like a normal record; these components are intended to be shared among multiple tasks. The protected operations of the protected record provide synchronized access to the components. - Protected records have three kinds of protected operations: protected functions, protected procedures, and entries. Protected functions and protected procedures are known as protected subprograms. (Note that task entries are also considered protected operations. This is not discussed any further here.) - Protected subprograms provide exclusive read-write access to the record components. Protected functions, since they cannot change the protected record components, may be optimized to use a shared read-only lock. - Protected record entries also provide exclusive read-write access to the record components, but in addition, they specify a barrier, which is a Boolean expression that generally depends on the components of the protected record. The Run-time System ensures that the barrier is true before allowing a protected record entry call to proceed. - Each protected record object has a conceptual lock associated with it. (This lock may sometimes be an actual one, or can instead be implemented using the ceiling priorities model [see MS-H, Real-Time Systems Annex]). Before operating on the protected record (that is, before entering a protected subprogram or protected record entry), the calling task seizes the lock. Evaluation of barriers, execution of protected operation bodies, and manipulation of entry queues (see below) are all done while the lock is held. On a multi-processor, the intended implementation of locks uses busy waiting. (Other, more specialized algorithms, are allowed). - There is a queue associated with each protected record entry. Tasks wait in the queue until the entry's barrier becomes TRUE. If the barrier is already TRUE when the entry call first seizes the lock, then it is executed immediately; the queue is not used. While waiting in the queue, a task does not hold the lock. - A requeue statement is allowed in an entry body (and an accept statement). The effect of this statement is to return the current caller back to the queue, or to place it on another, compatible, entry queue (see MS-9.7.1). - MS-H, Real-Time Systems Annex associates ceiling priorities with protected records. As is further explained by the corresponding rationale section, the ceiling rules prevent the priority-inversion phenomena, and ensure freedom from deadlocks on single-processor systems. 4.4.3. The Requeue and Selective Entry Call Facilities In addition to the protected record facility, the 9X proposal adds the selective entry call construct and the requeue statement. These two features address the requirements for support of Common Real-Time Paradigms, and Non-Blocking Communication. These features are designed to complement the protected record feature, and to provide an integral solution that will compose and scale up naturally to large and complex applications, involving multiple asynchronously communicating tasks. These features are important in building the next generation of systems, which will be multi-mode, fault-tolerant, and composed of multiple subsystems. 4.4.3.1. The Purpose of the Requeue Statement When constructing components such as complex servers or user-defined schedulers, the need often arises for entry or accept bodies to determine the order and the timing of the service based on various controlling parameters: These parameters may be local to the server, and dependent on its own state, be an attribute of the client or the controlled task, or be global to the system. In addition, these parameters may often change from the time the entry call is made to the time the selection decision is made. For fairly simple cases, i.e. when the parameters are known to the caller, do not change from the time of call, and have a relatively small discrete range, the entry family facility of Ada 83 might suffice (see [Wellings 84], [Burger 87], [Burns 87]). However, when those restrictions do not hold, a more powerful mechanism is often needed. In the literature, the terms availability controls and race controls have been associated with those requirements. These can be further classified as preference, mutual, consensus, forerunner (and other) controls. The classification is used to distinguish the various cases mentioned above, and to facilitate a better discussion of the appropriate solutions [Elrad 88]. To provide, in a programming language, the full expressive power to describe the above controls would require an elaborate semantic structure and a complex (and potentially large) run-time support system. Instead, we have chosen to provide a single, simple statement in Ada 9X, to allow the programmer to construct the desired control algorithms based on the specific application needs and trade-offs. In order for the server to gain access to the caller's parameters, a context switch is required, but there is no need to resume the caller and to require it to initiate another entry call based on the results of the first; it may simply be moved to another entry queue. An alternate approach that was proposed required the caller to first query the state of the server, and then to initiate an entry call with appropriate parameters (presumably using a specific family member index) to reflect the server's state. This approach suffers from the potential of race conditions, since no atomicity is guaranteed between the two calls (another caller may be serviced and the state of the server may be changed), so the validity of the second request which is based on the first, may be lost. In the case of protected record entry calls, exclusive access is maintained throughout the period of examining the parameters and doing the requeue; for the case of accept bodies, the server task controls its own state and since it can refuse to accept any intermediate calls, the atomicity is also maintained. With the requeue statement, with abort may be specified. In Ada 83, after the rendezvous is started, there is no way for the caller to cancel the service request, or for the time-out parameter to take effect (the time-out request is present only until the acceptor starts the service). There is a good reason for this behavior: after the service has commenced, the server is in a temporary state, and removing the caller asynchronously can invalidate the server's data structures. In addition, because of by-reference parameters, the acceptor must maintain its ability to access the caller's data area (e.g. the stack). If the caller ``disappears'', this might result in dangling references. However, in some cases, deferring the cancellation of the call is unacceptable, in particular when the time-out value is needed to control the amount of time until the service is completed (as opposed to just started). With the addition of asynchronous transfer of control to Ada 9X, the same situation can arise if the caller is ``interrupted'' and must change its flow of control as soon as possible. Since there is not a single best approach for all applications, and since no easy work-around exists, the with abort is provided to allow the programmer to choose the appropriate mechanism for his needs. In general, when cancellation during a requeue is to be allowed, the server will ``checkpoint'' its data-structures before issuing requeue with abort, in such a way that if the caller is removed from the second queue, the server can continue to operate normally. When this is not possible, or when the cancellation during a requeue is not required, a simple requeue will suffice, and will hold the caller until the service is fully completed. 4.4.3.2. The Purpose of the Selective Entry Call Facility In any communication system with multiple correspondents, it is important to be able to simultaneously wait for multiple callers or events. In Ada 83, the selective accept statement provides this capability in conjunction with the synchronous rendezvous mechanism. For Ada 9X, we have proposed an analogous mechanism to support asynchronous communication, the selective entry call. When converting Ada 83 programs that use rendezvous to take advantage of the new protected record feature, it is important that users will not find themselves hampered by the lack of an equivalent functionality to wait for multiple events. For example, in Ada 83, a mailbox facility will be implemented using intermediary tasks to control the mailbox. In Ada 9X, the same can be accomplished with a protected record. There are cases when a reader wants to wait for more than one message at the same time. Often, the order in which these messages will arrive is not known a priori (i.e. at compile-time) since there is no inherent ordering among the messages (for example, a message router in a network that passes along packets coming from different ``directions'' and headed for different destinations). The work-around of using polling techniques will severely reduce the bandwidth of the service and the utilization of the system, and is therefore considered unacceptable in many situations. In other cases, there is a need for a client, while waiting for a service to be completed, to wait on another, high-priority channel. Such a channel may be an input device (sending operator commands or external events), a watch-dog timer, or something else. Situations like this arise quite often in real-time and multi-mode systems with short response-time requirements, or when developing user-defined schedulers. With this facility, the client may, at the same time, wait for the ``normal'' service to be completed, while also ``listening'' to the other channels. With a regular selective entry call, the alternate channels are listened to as long as none of the other services has begun. With the abortable final part, the listening continues further and, when needed, can abort the executing service. Finally, it is quite common that more than one such high-priority channel must be monitored at the same time. A common example of such a case is an algorithm that must be bounded in time (and therefore be stopped if it overruns), but must also be responsive to a mode-change command coming from the system's operator. In some, simple cases, the delay alternative is usually sufficient. However, when more events are expected in addition to the time expiration, or a user-defined timer is involved (one that uses protected record entries to implement a user-specific timing facility), more than one entry call alternative is needed. Making the selective entry call fully generalized, i.e. allowing one to mix entry calls and accept statements in the same select construct was found to be too costly in terms of implementation (it would require a distributed hand-shake protocol among tasks), and too complex semantically. Thus, it was disallowed. Below we show an example of coding a program-wide mode-change operation. This example also uses the proposed feature of asynchronous transfer of control. -- Define enumeration of system modes type SYSTEM_MODE is (NORMAL, MODE2, MODE3, ...); -- Define singleton protected record to keep track of -- current mode and to broadcast mode changes protected MODE_MANAGER is procedure SET_MODE(M : SYSTEM_MODE); -- Set new mode function GET_MODE return SYSTEM_MODE; -- Retrieve current mode entry WAIT_FOR_MODE_CHANGE(SYSTEM_MODE) ( NEW_MODE : out SYSTEM_MODE); -- Entry family to wait for a mode change. -- Family index is mode of calling task; -- entry returns when system mode does not equal -- mode of caller, with New_Mode indicating -- new system mode. private record CURRENT_MODE : SYSTEM_MODE := NORMAL; -- current system mode end MODE_MANAGER; protected body MODE_MANAGER is procedure SET_MODE(M : SYSTEM_MODE) is begin -- Record new mode CURRENT_MODE := M; end SET_MODE; function GET_MODE return SYSTEM_MODE is begin -- Return current mode return CURRENT_MODE; end GET_MODE; entry WAIT_FOR_MODE_CHANGE (for OLD_MODE in SYSTEM_MODE) (NEW_MODE : out SYSTEM_MODE) when OLD_MODE /= CURRENT_MODE is begin -- Return current mode, since it doesn't equal -- mode of caller NEW_MODE := CURRENT_MODE; end WAIT_FOR_MODE_CHANGE; end MODE_MANAGER; -- Here is a typical task which reacts to mode changes: task body TYPICAL_TASK is MY_MODE : SYSTEM_MODE := NORMAL; NEW_MODE : SYSTEM_MODE; begin loop select MODE_MANAGER.WAIT_FOR_MODE_CHANGE (MY_MODE)(NEW_MODE); -- Mode changed, set new local mode and loop around MY_MODE := NEW_MODE; then abort -- Case on current task mode and do appropriate -- processing, being careful to protect key data -- structures from abort. case MY_MODE is when NORMAL => ... -- Do Normal Mode Processing when MODE2 => ... -- Do Mode 2 Processing when MODE3 => ... -- Do Mode 3 Processing . . . end case; end select; end loop; end TYPICAL_TASK; 4.4.4. Benefits and Intended Use Protected records combine high efficiency with generality; therefore, they can be used as building blocks to support various common real-time paradigms. This section shows some examples of how protected records can be used: - An implementation of indivisible counters shows how protected subprograms are used. - An implementation of persistent signals shows how protected record entries are used. - An implementation of broadcast shows how requeue statements are used. - An implementation of a timer interrupt handler shows how interrupts are handled by attaching protected procedures to interrupts. - A disk manager implementation illustrates how requeue statements can be used to implement preference control. The protected records feature allows Ada 9X to support these and other real-time paradigms with a smaller overall change to the language than the alternative approach where each problem is solved with its own distinct feature. In the following examples, we refer to the lock as an actual object with lock and release operations. This, of course, is not required, and is done for ease of presentation only. 4.4.4.1. Indivisible Counter Increment The first example shows a counter that is shared among multiple tasks: protected COUNTER is procedure INCREMENT(NEW_VALUE: out POSITIVE); private record DATA: INTEGER := 0; end COUNTER; ... protected body COUNTER is procedure INCREMENT(NEW_VALUE: out POSITIVE) is begin DATA := DATA + 1; NEW_VALUE := DATA; end INCREMENT; end COUNTER; The counter is initialized to zero. A task may increment it by calling the INCREMENT procedure: COUNTER.INCREMENT(NEW_VALUE => X); If N tasks do so, each exactly once, they will each get a unique value in the range 1..N. Note that without the synchronization provided by the protected record, multiple simultaneous executions of INCREMENT might cause unpredictable results. With the protected record, a task that calls INCREMENT will first seize the lock, thus preventing such simultaneous executions. Since there are no entries here, there are no queues. The protected record consists of a lock and the component DATA. If we want to define many COUNTER objects, we would change the above example to declare a protected record type instead of a protected record object, like this: protected type COUNTER_TYPE is ... -- same as before end COUNTER_TYPE; ... COUNTER_1, COUNTER_2: COUNTER_TYPE; -- declare two counters type MANY is array(1..1000) of COUNTER_TYPE; X: MANY; -- declare 1000 counters Note that the lock is associated with each protected record object, not with the type. Thus, each of the record objects in the above example has its own lock, and the data in each is independent of all the others. Note that INCREMENT has a short, bounded-time algorithm; all it does is to increment the value and assign it to the out parameter. This is typical: because protected record locks are intended to be implemented as busy-waits (at least on multi-processors), it is unwise to write an algorithm that might hold a lock for a long or unknown amount of time. This example is intended to demonstrate typical use of protected operations; since they represent a critical resource, they should be programmed accordingly. A common approach would be to just record the new state, under the protection of the lock, and do the actual processing outside the protected record body. 4.4.4.2. Persistent Signals The following example shows a persistent signal facility. In this example, tasks may wait for an event to occur. When the event occurs, some task whose job it is to notice the event will ``signal'' that the event has occurred. The signal causes exactly one waiting task to proceed. The signal is persistent in the sense that if there are no tasks waiting when the signal occurs, the signal persists until a task invokes a wait, which then consumes the signal and proceeds immediately. Multiple signals when no tasks are waiting are equivalent to just one signal. Note that even though ``persistence'' semantics have been chosen for the example, the protected record semantics are indifferent to this property, transient semantics can also be programmed but are not shown here. Note that persistent signals are isomorphic to binary semaphores; the wait operation is P, and the signal operation is V. protected EVENT is entry WAIT; -- Wait for the event to occur. procedure SIGNAL; -- Signal that the event has occurred. private record OCCURRED: BOOLEAN := FALSE; end EVENT; ... protected body EVENT is entry WAIT when OCCURRED is begin OCCURRED := FALSE; -- consume the signal end WAIT; procedure SIGNAL is begin OCCURRED := TRUE; -- Make WAIT's barrier become TRUE. end SIGNAL; end EVENT; The intended use is that a task waits for the event by calling: EVENT.WAIT; The signaling task notifies the waiting task by calling: EVENT.SIGNAL; The example works like this: If a WAIT occurs first, the task will seize the lock, check the barrier, and find it to be FALSE. Therefore, the task will add itself to the entry queue, and release the lock. A subsequent SIGNAL will seize the lock and set the OCCURRED flag. Before releasing the lock, the signaling task will check the WAIT entry queue. There is a task in it, and the barrier is now TRUE, so the body of WAIT will now be executed, setting the flag to FALSE, and the WAITing task released. Before releasing the lock, the process of checking entry queues and barriers is repeated. This time, the WAIT barrier is FALSE, so nothing happens; the lock is released, and the signaling task goes on its way. If, on the other hand, a SIGNAL occurs first, then the protected procedure body will seize the lock, set the flag to TRUE, find nothing in the entry queues, and release the lock. A subsequent WAIT will seize the lock, find the barrier to be TRUE already, and proceed immediately with its body. The barrier is now FALSE, so the WAITing task will simply release the lock and proceed. Important things to note: - Protected subprograms do not have barriers. Protected record entries always have barriers. SIGNAL is a protected procedure, because it never needs to suspend waiting for the state of the protected record to change; that is, it needs no barrier. WAIT is an entry, because it needs to suspend until the flag is set before proceeding. - The protected record entry queues are not used for tasks waiting for access to the protected record data. They are used for tasks waiting for an entry barrier to become true; that is, they are waiting for the state of the protected record to change to some user-specified value. The lock associated with the protected record synchronizes access to the shared data. The same lock also protects the entry queues themselves; the queues may be considered as part of the shared data. - Barriers are associated with entry queues, not with individual tasks calling the entries. Therefore, the number of barriers to be evaluated can never be more than the number of entries declared in the protected record type. - Entry barriers are re-evaluated only at well-defined points: in particular, when a protected procedure or protected record entry associated with that protected record has just finished, but before it has released the lock. - Protected functions do not change the state of the protected record, and so should not change barrier values. Therefore, entry barriers are not re-evaluated when a protected function has just finished. - The lock stays locked during the actions that happen at the end of a protected procedure or protected record entry (that is, checking the queues for non-empty status, evaluating barriers, removing a task from a queue, and executing the entry body). This means that tasks already on the queues get preference over tasks that are trying to seize the lock. - Barriers, like protected operation bodies, should contain only short, bounded-time expressions. In typical examples, the barrier simply tests a BOOLEAN flag, or checks the number of elements in an entry queue. The programmer has complete control over worst-case waiting times; this worst-case analysis can be done by inspecting the algorithms in the protected bodies, the barriers, and the maximum number of tasks that can be expected to be waiting on each barrier. - The language does not specify what task executes a particular barrier or entry body. One can even think of barriers and entry bodies as being executed by the Run-time System, not by any particular task. Certain restrictions on what may appear in a barrier or an entry body imply that it does not matter which task does the work. This is done to ensure that no context switches are required; when one task finishes executing an operation, the task can immediately execute any others that have become ready, without having to switch to the context of the ``correct'' task. 4.4.4.3. Broadcast The following example shows a broadcast facility. As in the previous signal example, tasks wait for an event to occur. However, this is a ``broadcast,'' because when the event is signaled, all waiting tasks are released, not just one. After releasing them, the event reverts to its original state, so tasks can wait again, until another signal. Note also, that unlike the previous example, the event here is not persistent. If no tasks are waiting when the signal arrives, it is lost. protected EVENT is entry WAIT; entry SIGNAL; private entry RESET; record OCCURRED: BOOLEAN := FALSE; end EVENT; ... protected body EVENT is entry WAIT when OCCURRED; -- no body; same as "null;" entry SIGNAL when TRUE is -- TRUE barrier; don't suspend on initial entry begin if WAIT'COUNT > 0 then OCCURRED := TRUE; requeue RESET; -- Suspend on the RESET entry. end if; end SIGNAL; entry RESET when WAIT'COUNT = 0 is begin OCCURRED := FALSE; end RESET; end EVENT; The intended use is that tasks wait for the event by calling: EVENT.WAIT; Another task notifies them that the event has occurred by calling: EVENT.SIGNAL; This causes all currently waiting tasks to continue, and the event to be reset, so that future calls to EVENT.WAIT will wait. The example works like this: If a task calls EVENT.WAIT, it will first seize the protected record lock. It will check OCCURRED, find it to be FALSE, add itself to the entry queue, and release the lock. Several tasks might add themselves to the queue in this manner. Later, the signaling task might call EVENT.SIGNAL. After locking the protected record, the task will execute the entry body (since its barrier is TRUE). If no tasks are currently waiting, the task exits without updating the flag. Otherwise, it sets the flag to indicate that the event has occurred, and requeues itself on the RESET entry. (RESET is declared in the private part, because it is not intended to be used directly by clients of the protected record.) Before releasing the lock, the signaling task will check the queues. The barrier for WAIT is now TRUE. A task is chosen from the WAIT queue, and allowed to proceed. Since the entry body for WAIT does nothing, the flag will not change. (See MS-H, Real-Time Systems Annex for the detailed rules for choosing among the tasks waiting in entry queues.) This sequence of events will be repeated until the entry queue for WAIT is empty. When the WAIT queue is finally empty (e.g., COUNT equals 0), then RESET's barrier is TRUE, and the RESET operation is executed, resetting the flag. The queues are now empty, so the protected record lock can be released. Note that the implementations can optimize null entry bodies by releasing waiting tasks in one operation, when the barrier is true. Because the steps described in the last two paragraphs are executed with the protected record locked, any other tasks that try to WAIT or SIGNAL during that time will have to wait for the queues to clear out, as explained above. Furthermore, there are no race conditions caused, for example, by the barrier's value changing between the time it is evaluated and the time the corresponding entry body is executed. The check for now-TRUE barriers, happens whenever the state of the protected record might have changed: that is, they happen just after executing the body of a protected procedure or protected record entry. Important things to note: - Several potentially suspending constructs are defined in MS-9.7(21-28). Seizing a protected record lock is not considered to be such an operation. Note that suspension is defined entirely by the language; if the Run-time System queues tasks internally for some other reason, and makes them wait, this is not considered suspension. The key difference between protected procedures and protected record entries is that calling the former may not suspend, while the latter may. It is important that the programmer can tell the difference; therefore, the declarations are introduced by a different keyword, and the difference appears in the specification. It is also important that the compiler can tell the difference; this allows it to know how many queues to allocate without seeing the protected body. Protected subprogram bodies are allowed to call other protected subprograms, because that does not involve suspension. Protected record entry calls suspend initially if the barrier is FALSE. In addition, entries may suspend by executing a requeue statement. These are the only ways in which a protected record entry may suspend; it may not, for example, execute a delay statement. - A consequence of the non-suspension rule is that protected subprograms cannot requeue. Only entries can requeue. - A requeue statement may requeue on the same entry, on a different entry of the same protected record, or even on an entry of a different protected record. Any actual parameters to the original call are passed to the new entry; therefore, the new entry must have the same parameter profile, or else no parameters at all. - In the broadcast example, the requeue statement prevents a race condition that might otherwise occur. For example, if the signaling task were required to call SIGNAL and RESET in sequence, then the releasing of WAITing tasks would no longer be atomic. A task that tried to WAIT in between the two calls of the signaling task, might have been released as well. That might even be the same task that was already released once by that SIGNAL. - Requeue is allowed from accept bodies as well. The target entry may belong to either a task or to a protected record. We don't discuss this aspect any further here. 4.4.4.4. Queued Waiting Protected record locks provide busy waiting. Some applications require queued waiting instead, and this can easily be programmed in terms of protected records. Look at the example of counting semaphores in MS-9.6. This example shows a simple form of queued waiting: a task that calls ACQUIRE will wait in ACQUIRE's entry queue until the barrier for that queue (CURRENT_COUNT > 0) becomes TRUE. Since CURRENT_COUNT represents the number of available resources, if there are none available, the calling task will be queued. It is important to note that the protected record lock does not provide the queued waiting in this example. The lock simply protects the protected record data: the CURRENT_COUNT and the queue itself. Tasks do not wait on the queue for access to the protected record; instead, this protected record implements queued waiting on some other application-defined resource. This queued waiting is efficient: The barrier will be evaluated just once when a resource is released, which is exactly what one would expect of a semaphore-like construct. 4.4.4.5. Priorities This section briefly discusses priorities. The detailed rules may be found in MS-H, Real-Time Systems Annex. Each task has a base priority, which may be set dynamically by the user. (If the user does not specify the base priority, a default value is used.) Each task also has an active priority, which depends in part on the base priority. Initially, the active priority is equal to the base priority. The active priority is raised during certain tasking operations; the active priority is always at least as high as the base priority. The active priority is used by the run-time system when making various priority-based decisions. In addition, each protected record has a ceiling priority. The ceiling priority is the upper bound for the active priorities of tasks that may call protected operations of that protected record. When such an operation is called, the active priority of the calling task is raised to the ceiling of the protected record object. The scheduling policy defines how the Run-time System allocates various resources to tasks, and how a task's active priority is determined. The active priority is usually determined from the task's base priority and the ceiling priorities of any protected records of which the task holds the lock. Ada 9X allows implementations to define and support other scheduling policies, provided they are invoked by pragmas and are compatible with the dispatching model. The core part of the language defines a FIFO queuing policy, and the real-time section adds a priority queuing policy. Here again, implementations have the freedom to add more specialized, application-derived policies. Ada 9X adds to the FIFO queuing and priority-based scheduling of Ada 83, a priority queuing and a more deterministic scheduling policy. It also allows implementations to add their own policies, under proper documentation, in order to facilitate scheduling techniques that may emerge in the future. Most tasks have priorities in the range of SYSTEM.PRIORITY. The range SYSTEM.INTERRUPT_PRIORITY defines interrupt priority levels; these are higher than SYSTEM.PRIORITY'LAST. However, tasks may run at interrupt priority levels as well. They can do so by explicitly raising their priority, or call protected operations of protected records with a ceiling priority in the SYSTEM.INTERRUPT_PRIORITY range. These priority rules are more elaborate than those of Ada 83. However, they accommodate the real-time needs for dynamic priorities. They also provide a more deterministic model for schedulability analysis of real-time systems. 4.4.4.6. Interrupts The Ada 9X interrupt-handling mechanism is based on the protected record and priority features of the language. (See MS-H, Real-Time Systems Annex.) Interrupt handlers are parameterless protected procedures associated with library level protected records. A protected record containing an interrupt handler must have a ceiling priority in the range of SYSTEM.INTERRUPT_PRIORITY. Thus, the interrupt handler runs at an interrupt priority level, effectively disabling other interrupts at the same or lower levels. Normal tasks can disable interrupts by setting their priority to an interrupt priority level or by calling protected operations of protected records with ceilings in the corresponding range. The predefined package INTERRUPT_MANAGEMENT contains types and constants for naming interrupts, and subprograms for attaching interrupts to handlers dynamically. The operations ATTACH_HANDLER and DETACH_HANDLER attach and detach interrupt handlers to and from interrupts, respectively. When an interrupt occurs, the attached handler, if any, is invoked. The child package INTERRUPT_MANAGEMENT.INTERRUPT_NAMES contains declarations of implementation- defined interrupt names, as appropriate for the particular hardware environment. It is natural to define interrupt handling in terms of protected records, because protected records already solve the key interrupt handling issues: Protected record operations execute in a limited context, because of the need to avoid context switching. Interrupts execute in a limited context, because of hardware-oriented efficiency considerations. Neither protected record operations nor interrupt handlers should suspend. The priority mechanisms associated with protected record operations are similar to the interrupt priority levels typically provided by the hardware. Finally, by having interrupt handlers be protected procedures, data sharing between the interrupt handler and ``normal'' tasks can be achieved in a natural way. Examples of using package INTERRUPT_MANAGEMENT: use INTERRUPT_MANAGEMENT; protected type TIMER is entry WAIT_FOR_TICK; procedure HANDLE_TIMER_INTERRUPT; function GET_CLOCK return TIME_TYPE; private record CLOCK : TIME_TYPE; TICK_OCCURRED : BOOLEAN := FALSE; end TIMER; protected body TIMER is entry WAIT_FOR_TICK when TICK_OCCURRED is begin TICK_OCCURRED := FALSE; end WAIT_FOR_TICK; procedure HANDLE_TIMER_INTERRUPT is begin ... -- Update the clock. TICK_OCCURRED := TRUE; end HANDLE_TIMER_INTERRUPT; function GET_CLOCK return TIME_TYPE is begin return CLOCK; end GET_CLOCK; end TIMER; ... MY_TIMER : TIMER; ... ATTACH_HANDLER (MY_TIMER.HANDLE_TIMER_INTERRUPT'ACCESS, INTERRUPT_NAMES.TIMER_INTERRUPT_ID); ... 4.4.4.7. Disk Manager Many real-time applications require preference control, where the satisfiability of a request depends on the parameters passed in by the calling task and often also on the internal state of the server. Examples are: - The server must serve higher-priority requests first, where the priority of the request is passed as an entry parameter. - A particular entry call is used to request any of several resources, where some resources might be available, while others are in use. An entry parameter indicates which of the resources the task is requesting. - Several copies of a resource may be allocated at once, where the calling task passes in the number of required resources. A specific instance of this situation is a memory allocator, where the resource is a block of storage units. The calling task asks for a particular number of storage units, and must wait until a contiguous portion of memory of at least the right size is available. It might be that a request for 100 storage units can be satisfied, whereas a request for 1000 storage units cannot. - The server is controlling a device that might be ready to serve some requests but not others. The following is an example of the last situation: we have a disk device with a head that may be moved to different tracks. When a calling task wants to write to the disk at a particular place, the call may proceed immediately if the disk head is already on the right track. Otherwise, the disk manager tells the disk device to move the head. When the disk is done moving the head, it generates an interrupt. While waiting for the interrupt, the calling task is suspended. Preference control is implemented in Ada 9X using the requeue statement. The entry call proceeds whether it can be satisfied or not. Then the server checks whether the request is satisfiable by looking at the parameters. If so, the request is satisfied, and the entry returns. If not, the request is requeued to another entry, to wait until the conditions change. The preference control in our example is simple: we can satisfy I/O requests for the current disk track, and queue the other. Since the disk address is passed-in as an entry parameter, some calls to the WRITE entry can proceed, while others cannot. protected DISK_MANAGER is entry WRITE(WHERE: DISK_ADDRESS; DATA: DISK_BUFFER); -- Write data to the disk at the specified address. entry READ(WHERE: DISK_ADDRESS; DATA: out DISK_BUFFER); -- Read data from the disk at the specified address. procedure DISK_INTERRUPT; -- Called when the disk has interrupted, indicating -- that the disk head has moved to the correct track. private entry PENDING_WRITE(WHERE: DISK_ADDRESS; DATA: DISK_BUFFER); entry PENDING_READ(WHERE: DISK_ADDRESS; DATA: out DISK_BUFFER); record CURRENT_DISK_TRACK: DISK_TRACK_ADDRESS := ...; -- Track where the disk head currently is. OPERATION_PENDING: BOOLEAN := FALSE; -- Is an incomplete READ or WRITE operation pending? DISK_INTERRUPTED: BOOLEAN := FALSE; -- Has the disk responded to the move command with -- an interrupt? end DISK_MANAGER; In order to write on the disk, a task calls DISK_MANAGER.WRITE, passing the disk address and data as parameters. The READ operation is similar; we do not show it in full here. protected body DISK_MANAGER is procedure DISK_INTERRUPT is begin DISK_INTERRUPTED := TRUE; end DISK_INTERRUPT; entry PENDING_WRITE(WHERE: DISK_ADDRESS; DATA: DISK_BUFFER) when DISK_INTERRUPTED is begin CURRENT_DISK_TRACK := WHERE.TRACK; -- We know that the disk head is at the right track. ... -- Write DATA to the disk. OPERATION_PENDING := FALSE; -- Allow READS and WRITES to proceed. end PENDING_WRITE; entry WRITE(WHERE: DISK_ADDRESS; DATA: DISK_BUFFER) when not OPERATION_PENDING is begin if (WHERE.TRACK = CURRENT_DISK_TRACK) then ... -- Write DATA to the disk. else ... -- tell the disk to move to the right track OPERATION_PENDING := TRUE; -- Prevent further READS and WRITES. requeue PENDING_WRITE; -- Wait for the interrupt. end if; end WRITE; entry PENDING_READ(WHERE: DISK_ADDRESS; DATA: out DISK_BUFFER) when DISK_INTERRUPTED is begin ... -- similar to PENDING_WRITE end PENDING_READ; entry READ(WHERE: DISK_ADDRESS; DATA: out DISK_BUFFER) when not OPERATION_PENDING is begin ... -- similar to WRITE end READ; end DISK_MANAGER; The WRITE operation checks whether the disk head is already on the right track. If so, it writes the data and returns. If not, it sends a command to the disk telling it to move the head to the right track, and then requeues the caller on PENDING_WRITE. It sets a flag to prevent intervening WRITE and READ operations. When the disk has completed the move-head command, it interrupts, causing the DISK_INTERRUPT operation to be invoked. The DISK_INTERRUPT operation sets the flag that allows the PENDING_WRITE operation to proceed. We do not specify here how DISK_INTERRUPT gets called when the interrupt occurs. It might be attached directly to the interrupt, or some other interrupt handler might call it or set a flag in some other protected record that causes DISK_INTERRUPT to be called. The details are not important to this example. A real disk manager would be more complicated; it would probably allow multiple pending requests, sorted by track number, the actual reading and writing might be interrupt driven (in addition to the disk head movement), and so on. We are keeping the example simple, in order to illustrate the key features of preference control. Important things to note: - One might think that an obvious way to implement preference control would be to make the entry barrier depend on the entry's parameters. However, that is illegal. It is disallowed in order to allow for efficient implementations and to avoid everly-complex semantics for the user. Because the barriers do not depend on the formal parameters, the value of the barrier is the same for all callers of the same entry. This means that there is only one barrier value per entry, not one per entry call, so evaluation of barriers can be efficient. If the barrier expression can be evaluated in a bounded amount of time, as is usually the case, the programmer can calculate total worst-case barrier-evaluation times based on the worst-case barrier evaluation times of each of the entries. - It is important that the decisions of when to service a request happen inside the protected record, because these decisions are based on the protected record local data, which, of course must be protected. The Ada 9X requeue mechanism achieves that. - The above example works properly in the presence of abort and asynchronous transfer of control. For example, if a writing task is aborted while it is waiting on the PENDING_WRITE queue, the abort will be deferred until after PENDING_WRITE has been executed. On the other hand, the programmer might wish to allow the task to be aborted earlier. In that case, the requeue statement would say: requeue PENDING_WRITE with abort; Then, the protected body would have to be written in such a way that callers can silently disappear from the PENDING_WRITE queue without disrupting things. Thus, the programmer of the server gets a choice: aborts can be allowed or prevented during requeue'd waiting. - In Ada 83, an extra ``agent'' task is required to ``hold'' the call in examples like this. In order to correctly handle abort and asynchronous transfer of control, and still handle multiple outstanding requests, the agent tasks need to be created dynamically, as needed. Such agent tasks are too expensive for many applications, and their timing behavior is too unpredictable. - Other methods in Ada 83 require exporting several entries that must be called in a particular order. This violates information-hiding principles, and causes race conditions, because events can occur between the multiple calls. The information-hiding objection can be answered by putting the task in a package, and putting the correct pattern of entry calls in a procedure exported from the package, thus enforcing that protocol. However, that exported procedure cannot be used in timed, conditional, and selective entry calls. (Note that it is not possible to make that procedure into a protected record entry, because entry bodies are not allowed to suspend. The non-suspension rule is essential for preventing deadlock.) Other solutions to the race problems generally require the requesting task to poll the server, which is inefficient and non-deterministic, in addition to being error-prone. 4.4.5. Relationship to Previous Work Protected records are related to two other synchronization primitives: the conditional critical region, and the monitor. The protected record has been incorporated in a way that is compatible with Ada's existing task types, entries, procedures, and functions. In 1973, Hoare proposed a synchronization primitive called a conditional critical region [Hoare 73]; its syntax is: region V when barrier do statements end; where the barrier is a BOOLEAN expression, and V is a variable. The semantics of the construct may be described as follows [Brinch-Hansen 73]: When the sender enters this conditional critical region, the [barrier expression] is evaluated. If the expression is true the sender completes the execution of the critical region ... But if the expression is false, the sender leaves the critical region temporarily and enters an anonymous queue associated with the shared variable V. However, Brinch-Hansen pointed out a flaw with conditional critical regions [Brinch-Hansen 73]: Although [conditional critical regions] are simple and well-structured, they do not seem to be adequate for the design of large multiprogramming systems (such as operating systems). The main problem is that the use of critical regions scattered throughout a program makes it difficult to keep track of how a shared variable is used by concurrent processes. It has therefore been recently suggested that one should combine a shared variable and the possible operations on it in a single, syntactic construct called a monitor. A monitor has a collection of data and subprogram declarations. In Ada terms, the subprograms declared in the visible part of a monitor, and which are therefore visible outside of the monitor, are guaranteed to have exclusive access to the monitor's data. The monitor may have some variables known as condition variables. These condition variables are like semaphores, in that they have WAIT and SIGNAL operations. A WAIT operation waits for a matching SIGNAL operation. Hoare introduces monitors with somewhat different syntax, but with equivalent semantics [Hoare 74]. Ada 9X protected records are an amalgam of conditional critical regions and monitors: they collect all the data and operations together, like monitors, and they have barriers, like conditional critical regions. 4.5. Summary This chapter has discussed the three major enhancements to Ada 9X in depth. We believe that these new building blocks will enable developers to solve application problems more effectively. The flexibility of the building block approach will also allow programmers to solve new problems that will challenge software developers in the 1990's and beyond. At this point, the reader has been introduced to the major changes proposed for Ada 9X, with particular emphasis on the most important new capabilities of Ada 9X. Appendix S provides a section-by-section rationale for chapters 1 through 14 of the Mapping Specification (4.0). For this release, Volume II contains the specifications and rationale for the Specialized Needs Annexes. S. Section-by-Section Rationale This appendix provides a section-by-section rationale for the Mapping Specification. Chapters and sections in the Mapping Specification correspond to sections and subsections within this appendix. If possible, the Mapping Specification should be read with the Ada 83 Reference Manual and this section-by-section rationale as companions, as they have a parallel structure. It is very important to note that just because a section of the Mapping Specification is empty, this does not imply that there will be no changes to the words in the Reference Manual (RM) in that section. Instead, we have chosen to organize the presentation of the mapping using RM sections because of its familiarity, but in no way is the Mapping intended to be an exhaustive definition of the Reference Manual wording changes. That task will be performed as part of the Revision Phase of the Mapping/Revision process. Note: This appendix was placed in Volume II in the previous release. S.1. Ada 9X Introduction S.1.1. Scope of the Standard S.1.1.1. Extent of the Standard S.1.1.2. Conformity of an Implementation With the Standard S.1.2. Structure of the Standard The main reason for including the Specialized Needs Annexes in the Ada language standard is to promote uniformity of implementations, and thereby promote portability and reuse of software components and designs. The specifications contained in these annexes provide capabilities that are needed frequently in certain application areas, though they are not needed so universally as to justify imposing them on every Ada implementation. The Specialized Needs Annexes provide standard interface specifications to meet the most common needs in these application areas. The all-or-nothing validation policy with respect to each Specialized Needs Annex is intended to avoid proliferation of large numbers of combinations of features available in different implementations, with a correspondingly large number of different kinds of validation certificates. Although implementations can implement part of a Specialized Needs Annex, they are encouraged to make that a temporary stop on the way to full compliance with the Annex. The Specialized Needs Annexes define conformance in such a way as to encourage uniformity of implementations, and to provide guidance for developing portable application code, without stifling invention. If an error-free program has certain semantics as defined by the core language, then it should have the same semantics in an implementation supporting some or all of the Specialized Needs Annexes. We wish to allow an implementation that provides features that go beyond the specifications of a Specialized Needs Annexes, and even options that diverge from that Annex, to still claim conformance to it so long as there is no impediment to a user who wants to get exactly the interfaces and behavior specified by this Annex. This means, in particular, that an implementation-defined extension should be allowed if it does not prevent a program that does not use that extension from being compiled, or change the run-time behavior of such a program. S.1.2.1. Rationale S.1.3. Design Goals and Sources S.1.4. Language Summary S.1.5. Method of Description and Syntax Notation S.1.6. Classification of Errors We have proposed to revise the error classification because the concept of ``erroneous execution'' was identified as a serious problem for formally reasoning about Ada 83 programs. We have introduced four categories, two of which relate to errors (erroneous executions and bounded errors (see MS-1.6), and two of which relate to portability (implementation defined and implementation dependent (see MS 1.1.2). The goals of this classification are to minimize the number of situations where execution is totally unpredictable, and to disallow the possibility of program error being raised in certain situations where it is currently allowed. The former goal is achieved by changing certain erroneous situations into bounded errors or implementation dependent or defined. The latter goal is achieved by changing certain erroneous situations into implementation dependent or defined. We have also proposed that an implementation may provide different modes of operation that control, for example, when a compilation is rejected. There must be a mode in which all legal compilations are accepted. In other modes, however, coding conventions, extra restrictions, etc. may be enforced by the compiler, resulting in otherwise legal compilations being rejected at compile-time. These restrictive modes allow the compiler to be a more active tool in supporting a specialized software engineering environment, or a special set of target hardware capabilities. However, as noted, the compiler must be capable of correctly compiling the full language. S.2. Ada 9X Lexical Elements As part of the original agreement between ISO and ANSI to accept ANSI/MIL-STD-1815A as an international standard, ANSI agreed to provide better support for international character sets in the first revision of Ada. Therefore, we are defining support for an 8-bit character set based on ISO-8859, and a 16-bit character set, probably based on DIS-10646 and Unicode. These extended characters will be usable in character and string literals, and in comments. It is still an open question to what extent extended characters should be allowed in identifiers. We are requiring that pragmas be restricted syntactically so that all compilers can at least parse them. Also, to improve error detection, we are requiring that compilers produce a warning when a pragma is unrecognized, and identify as an error a pragma that is misplaced or malformed. In Ada 83, it was permissible for compilers to ignore such pragmas without a warning, which could lead to unexpected behavior. We have formalized the definition of configuration pragmas to specify options that affect more than a single compilation unit (often an entire program). S.3. Ada 9X Declarations and Types S.3.1. Declarations The term view is introduced into the description to make it easier to separate properties associated with an entity from properties associated with a particular reference to an entity. For example, a type may have two views, one in places where its full declaration is visible, and one where the type is private. As another example, as a result of renaming, two subprogram names may denote the same subprogram, but with different formal parameter names associated with these two different views. S.3.2. Objects and Named Numbers S.3.3. Types and Subtypes We have generalized the term class to include user-defined classes defined by a type and all its direct and indirect derivatives. The concept of language-defined type classes (such as the discrete class, or the real class) allowed the description of Ada 83 to be more economical, and easier to understand. This same economy of definition and understanding is valuable for a user-defined type hierarchy forming a class. We have introduced a distinction between specific types and class-wide types. Specific types are those declared by type declarations, and correspond to Ada 83 types. Each specific type T has an associated class-wide type, T'CLASS. Class-wide types enable class-wide (polymorphic) programming, because a subprogram with a formal parameter of a class-wide type like T'CLASS accepts actual parameters for any type included in the class-wide type (i.e., T or any of its derivatives). In the implementation of such a subprogram, the operations of the root type (T in this case) are available. If the type is tagged (see MS-3.4.1), then it is also possible to write dispatching operations, which automatically dispatch to the appropriate implementation based on the type tag of the actual parameter. A class-wide operation of a tagged class-wide type usually calls one or more dispatching operations of the specific type. To simplify and unify the description of the existing language and the proposed Ada 9X features, we have adopted the terms elementary and composite for describing the two major categories of Ada types. Elementary types have no internal structure, and are used to represent simple values. Composite types are made up of components and other internal state, and are used to represent more complex values and objects. There are a number of existing Ada 83 rules, and proposed Ada 9X rules, that are made simpler by expressing them only in terms of elementary and composite types, rather than by enumerating more specific type classes. S.3.3.1. Type Declarations For uniformity, all types in Ada 9X are considered anonymous. Subtypes can have names. Therefore, the elaboration of a type declaration always creates an (anonymous, unconstrained, base) type, as well as a first-named subtype (that may be constrained). This change has no semantic effect; it is designed to simplify later description. In particular, a type mark is always the name of a subtype, and one need never say ``type or subtype.'' S.3.3.2. Subtype Declarations We have removed floating point and fixed point accuracy constraints from the syntax. Delta and digits may only be specified as part of a real type definition. ARG discussion in AI-571 concluded that reduced accuracy real subtypes should not be represented with reduced accuracy, making their usefulness in the language questionable. Removing reduced accuracy subtypes significantly simplifies the accuracy model of real types. S.3.3.3. Classification of Operations We have introduced the term primitive operations to encompass that set of operations that are ``tightly bound'' to a type, being either explicitly or implicitly declared at the point of the type declaration, and inherited by derivatives of the type. The more general term ``operation'' of a type, unchanged from Ada 83, includes any subprogram that has an operand or result of the type, independent of where the subprogram is declared. We wanted a term that represented the closed set of operations that effectively ``defined'' the semantics of a type. These we call the primitive operations. Ada 83 used ``implicit conversion'' to explain how integer literals were usable with any integer type, and how real literals were usable with any real type. For Ada 9X, we have adopted a similar mechanism as the basis for class-wide programming. However, rather than using the concept of implicit conversion, the static semantic rules are defined in terms of matching between actual parameters and formal parameters. As in Ada 83, if the actual parameter and the formal parameter are of the same type, then the actual matches the formal. However, the matching rules also allow certain other combinations. In particular, if a formal parameter is of a class-wide type, then the actual parameter may be of any type included in the class. This allows the definition of class-wide operations. This also allows a cleaner definition of operations from Ada 83 like the VAL attribute (which accepts an operand of any integer type), and fixed-fixed multiplication (which accepts operands of any two fixed point types). For types that are not tagged, the matching rules also allow an actual parameter of a class-wide type to match a formal parameter of any type included in the class. This allows, for example, integer literals (which are of the class-wide type root_integer'CLASS) to match a formal parameter of any integer type. This also allows a user to define an operation that returns T'CLASS, that can be used as an operand to an operation that expects T or one of its derivatives. This is essentially a user-defined ``literal.'' For example, if one defined a private complex-number type COMPLEX, one could define a (deferred) constant ZERO that would be usable with any derivative of COMPLEX, as follows: type COMPLEX is private; ZERO : constant COMPLEX'CLASS; -- class-wide ``literal'' zero function "+"(...) return COMPLEX; . . . private type COMPLEX is record REAL, IMAGE : FLOAT; end record; ZERO : constant COMPLEX'CLASS := (0.0, 0.0); In addition to class-wide matching, the operand matching rules cover the use of access parameters (see MS-3.9). When a formal is an access parameter, only the designated type of the actual parameter is considered for matching purposes. The actual matches the formal if their designated types are the same, or one is T while the other is T'CLASS. In addition, for tagged types, changes of representation are not permitted for derived types (see MS-13.4), so an actual also matches a formal access parameter if the designated type of the actual is included in the designated type of the formal. Access parameters allow operations to be defined that take access values rather than designated objects, while still keeping the operation a primitive operation of the designated type. With tagged types, this allows ``dispatching on access types'' without requiring the access value to be dereferenced first (see MS-3.4.2 for a discussion of dispatching operations). We have proposed that the attribute T'BASE may be used as a regular type name, rather than strictly as a prefix for other attributes. This is particularly useful within a generic package that might be instantiated with a constrained subtype, since the temporary variables used to perform a calculation may need to be unconstrained, even if the parameters and final result of an operation must satisfy the constraints of the actual constrained subtype. One potential problem with allowing the declaration of objects of subtype T'BASE is that the first named subtype (for example T) may have a size clause that takes advantage of the constraints on T. Objects of subtype T'BASE cannot generally be limited by the size specified for T. There are several reasons why this problem is not serious in practice: - Many compilers already use different sizes for different subtypes of the same type; - The construct for B in T'BASE'FIRST .. T'BASE'LAST loop ... is already legal in Ada-83 (presuming T is discrete), and is an existing way to effectively create a variable (B) of subtype T'BASE; - We have proposed that size clauses be applicable to subtypes other than the first named subtype, which will effectively require that compilers be able to represent different subtypes of the same type with different sizes. S.3.4. Derived Types For Ada 9X, we have chosen to build upon the Ada 83 derived type mechanism to provide for type extension (single inheritance) and run-time polymorphism, two fundamental features of object-oriented programming. Derived types are the existing type inheritance mechanism in Ada 83. If a new inheritance mechanism were introduced, perhaps based on ``package types'' or an explicit ``class'' construct, inheritance based on derived types would still remain as a redundant and complicating alternative inheritance mechanism. Since we have instead chosen to enhance the basic derived type mechanism, we have a single robust inheritance mechanism rather than two potentially conflicting and weaker ones. Rather than introducing an explicit class construct, we have instead chosen to support user-defined classes via a hierarchy of derived types. A class rooted at a type T consists of T and all of its direct and indirect derivatives. The existing Ada 83 rules for derived types ensure that all of the types in the class rooted at T have at least the same set of primitive operations as T, because a derivative may override and add operations, but it may not eliminate an operation inherited from the parent type. Having a set of operations that are well defined for all types in a class rooted at some type T makes it meaningful to construct class-wide operations that take advantage of this commonality. Much of the power and economy of object-oriented programming comes from the ability to write such class-wide operations easily. To accomplish this in Ada 9X we have generalized the Ada 83 concept of universal types beyond universal integer and universal real, so that every user-defined class of types may have its own class-wide (universal) type, which matches all types in the class (see MS-3.3.3). (Note that the term ``universal'' is no longer used; we make use of the new class-based terminology instead.) If an operation is explicitly defined on a class-wide type, then it becomes a class-wide operation via the operand matching rules. This is analogous to the integer literals in Ada 83, which are like parameterless functions usable for all integer types, because they yield a type universal_integer, which can be used anywhere some integer type is required. Ada 83 also has existing operations that take an operand of any type in a class (for example the VAL attribute takes a value of any integer type). These are therefore also class-wide operations. In Ada 9X, these are described as taking a parameter of the appropriate class-wide type (root_integer'CLASS for VAL), and then the operand matching rules allow a value of any type within the class as the actual operand (any integer type for VAL). S.3.4.1. Tagged Types and Type Extensions [new] The technique of using a class-wide type to define class-wide operations is quite useful when all types within the class are very similar (e.g. all integer types). However, without a run-time indication of the type of value being manipulated, the class-wide operation is necessarily limited to performing primitive operations according to the semantics associated with the root type for the class. When the types within a user-defined class require significantly different representations, and have significantly different implementations for their primitive operations, some run-time indication of the type becomes necessary. In Ada 83, this was generally accomplished using variant records with explicit discriminants and operations implemented with case statements. In Ada 9X, we have proposed that types may be tagged with an implicit discriminant that identifies the type and the implementations of its primitive operations. Class-wide operations on the class-wide type can then use this run-time type tag to dispatch automatically to the appropriate implementation of a primitive operation. Given a tagged record or private type, we have proposed that derivatives of the type may add components and discriminants. Such derivatives are called extensions of the parent record type. The run-time type tag for an extension identifies information that allows class-wide operations on the class-wide type to allocate, copy, compare for equality, and perform any other primitive operations on objects of the class-wide type, in accordance with the requirements of the particular extension identified by the tag. An explicit modifier tagged is used to signal the declaration of a record or private type that may be extended, and whose primitive operations, when applied to a class-wide type, provide run-time polymorphism. We considered making tagged the default, but the representation of a record that can be efficiently extended may not correspond to a record that cannot. Furthermore, to support run-time polymorphism, the implementation must create a type descriptor. This could represent an undue overhead if imposed on all record types. In addition to being used as formal parameters to class-wide operations, class-wide types may also be used as the designated type for an access type, and as the type of a declared object. Access types designating a tagged class-wide type are very important, since they allow the creation of heterogeneous linked data structures, such as trees, queues, etc., where the type tag of each element is used to identify the specific type of the element. Declared objects of a tagged class-wide type are not as frequently used as are formal parameters and heap objects, but they are useful as intermediates in larger computations. However, because there is no upper bound on the size of types in a class rooted at a tagged type, a declared object of a tagged class-wide type must be explicitly initialized to determine the size, the tag, and any discriminants for the object, and thereafter neither the tag nor the discriminants may be changed. If assignment were allowed to change the tag or the discriminants, then the size of the class-wide object might have to grow, requiring a deallocation and reallocation as part of assignment. We have avoided introducing operations in Ada 9X that involve this kind of implicit dynamic allocation at run-time. Therefore, an explicit access value with explicit deallocation and reallocation is required if a programmer desires to have the equivalent of an unconstrained object of a class-wide type. Note that Ada 83 requires a similar approach for handling unconstrained arrays and unconstrained discriminated types without defaults for the discriminants. In general, the way unconstrained composite types and their associated bounds or discriminants are handled in Ada 83 is a good model for how tagged class-wide types and their associated type tags are handled in Ada 9X. An explicit TAG attribute is defined for querying the tag of a tagged specific type, or of an object of a tagged class-wide type. This allows two class-wide objects to be checked to see whether they have the same tag. It is also possible to compare the tag of a class-wide object with the tag of a specific type; such a comparison is equivalent to a membership test (see MS-4.5.2). The TAG attribute may also be used for debugging, and for input/output of tagged values. S.3.4.2. Operations of Tagged Types [new] To implement a class-wide operation, the primitive operations associated with the root type of the class may be applied to the parameters (or other objects) of the class-wide type. If the root type is tagged, its primitive operations take advantage of the information provided by run-time type tags, to automatically dispatch to the implementation of the primitive operation identified via the tag. The primitive operations of a tagged type are called dispatching operations. Dispatching operations correspond to what are called virtual member functions and methods in other object-oriented programming languages (OOPLs). However, an important difference is that in Ada 9X, the operation is only dispatching when applied to an actual parameter of a tagged class-wide type. In other OOPLs, a dispatch is possible anytime an object reference or pointer is used as the prefix to the operation. In Ada 9X terms, what this means is that references/pointers in such OOPLs are always treated as though they designate a class-wide type. Ada 9X allows a (by-reference) formal parameter or an access value to have a specific type as its ``referent'' (this is the default, preserving upward compatibility). Ada 9X also allows a parameter or an access value to have a class-wide type as its referent, in which case the run-time dispatch is possible. Another difference is that, in Ada 9X, if a type T is tagged, then all of its primitive operations are dispatching operations; when passed a class-wide(We use the term ``class-wide'' as an adjective similar to ``integer'' -- an integer value is a value of an integer type; a class-wide operand is an operand of a class-wide type.) operand, they dispatch. In C++, only those particular member functions identified as virtual involve a run-time dispatch. In Ada 9X, a (non-dispatching) class-wide operation may be defined by explicitly declaring it with a formal parameter of type T'CLASS. No dispatch is performed in this case, because the body of a class-wide operation expects its formal parameter to still be class-wide. Note that, as in C++, a run-time dispatch may ultimately occur, when such an operation calls a dispatching (aka primitive or virtual) operation somewhere within its body. Another difference between Ada 9X and other OOPLs has to do with binary (or n-ary) operations of a type. In most OOPLs (CLOS is a notable exception), there is one parameter that is distinguished (normally by appearing as the prefix in a call), and controls the run-time dispatch. In Ada 9X, more than one operand, or even the result, may control the dispatch. For a primitive operation of a type T, the dispatching is controlled by the operands of type T, and the result if it is of type T. A final important difference between Ada 9X and some other OOPLs is that dispatching is safe in the sense that a call to a dispatching operation always has a well-defined implementation to dispatch to. In some OOPLs, such as Smalltalk, it is possible to send a message to an object that has no method for handling that message; a run-time error results. In Ada, such errors are always detected at compile time. When a primitive operation is called with class-wide operands in all controlling positions, a run-time check is made that all of these controlling operands have the same tag value, and the result is defined to return this same tag value. This common tag value is called the controlling tag value for the call, and identifies the specific type whose corresponding primitive operation is used to implement this call. This requirement that all controlling operands have the same tag value reflects an existing Ada 83 rule for derived types: The type of all operands of a parent type are systematically replaced with the derived type when inheriting a primitive operation. There are normally no primitive operations that manipulate a combination of types within the hierarchy. Each primitive operation operates only on one type within the class, and may return this same type. By treating all controlling operands symmetrically, we avoid some of the difficulties and anomalies encountered in other OOPLs with binary operations. By allowing the result context to control the dispatch, we allow parameterless functions to be used to represent type-specific literals, like an empty set in a tagged set class. There is no need to use run-time dispatch any time a controlling operand or result has a known specific type. In this case, if there are other controlling operands that are class-wide, then their tags are checked against the tag of this specific type. In any case, the specific type's implementation of the primitive operation is then called directly (this is effectively a case of ``static'' binding). The canonical implementation model for a type tag is a pointer to a run-time type descriptor, containing pointers to subprogram bodies implementing each of the primitive operations. This implementation model means that the call on a dispatching operation involves only tag-equality checks (if there is more than one controlling operand), and then a call through the appropriate subprogram pointer. The overhead for such a call is bounded, and can be kept to two or three instructions in most cases, ensuring that dispatching operations can be used even in demanding real-time applications. For a tagged type T, even the implicitly provided basic operations (e.g., OBJECT'SIZE, assignment if non-limited) will use dispatching when applied to a class-wide operand, to allow for new components that might be added by type extension. Generally, for each primitive operation of a parent type, a type extension may either inherit the original implementation, or it may override it. For an operation that had an operand of the parent type, if not overridden it becomes an operation with an operand of the type extension, which simply ignores (and does not affect) the extension part of the operand. However, for an operation that returned a result of the parent type, if not overridden, it becomes an abstract operation that has no implementation for the extension. This is because the extension part of the result would not be defined for such an operation. Abstract operations may also be explicitly declared (see MS-6.1), using the syntax: subprogram_specification is <>; Abstract operations allow a type to have a specification for an operation but no implementation for it, effectively requiring that each derivative define its own. Such operations have no default implementation, preventing a derivative from mistakenly inheriting a meaningless implementation. If a tagged type has an abstract primitive operation, then it is an abstract type, and no objects with a tag identifying that type may be created. This means that a call to an abstract operation will always dispatch to some non-abstract implementation that is defined for some derivative. No run-time check is needed to detect whether an operation is abstract, because no objects with the tag for an abstract type can be created. S.3.5. Scalar Types S.3.5.1. Enumeration Types S.3.5.2. Character Types As part of providing better support for international character sets, the fundamental character set of Ada 9X is being changed from the seven-bit ISO 646 standard, to the eight-bit ISO 8859 standard (which includes Latin-1, Latin-2, etc.). This means that the type CHARACTER in package STANDARD will now be an enumeration type with 256 positions, rather than just 128. This change is not directly upward compatible for programs that have arrays indexed by CHARACTER, or case statements over CHARACTER. However, the benefits of accommodating international character sets were felt to outweigh the costs of this upward incompatibility. The existing Ada 83 rules for completeness of array aggregates and case statements ensures that programs with upward incompatibilities will be easily caught, and easily fixed. To facilitate direct use of enumeration and string literals from all languages in the international community, we are including a WIDE_CHARACTER type in package STANDARD as well. The WIDE_CHARACTER type has 2**16 positions, and includes the enumeration literals of the first 128 positions of CHARACTER in a contiguous subrange. The particular character set has not yet been determined because there are currently two 16-bit character sets proposed for international standardization, the two-octet subset of DIS-10646, and Unicode. We expect that some merging of these two proposed standards will happen prior to Ada 9X standardization. We have also proposed a string type WIDE_STRING indexed by subtype POSITIVE, with component type WIDE_CHARACTER. S.3.5.3. Boolean Types S.3.5.4. Integer Types We have defined a class-wide type root_integer'CLASS to allow for the definition of integer-class-wide operations. In particular, integer literals and named numbers yield a value of this class-wide type. In Ada 83, integer literals and named numbers were of the type universal_integer, which was implicitly convertible to all integer types. In Ada 9X, we have folded Ada 83's special mechanism for handling numeric literals into the more general concept of class-wide matching. The net effect is the same, in any case. Integer literals and named numbers may be used anywhere a value of some integer type is required. However, we are also able to use the class-wide matching concept to explain the semantics of the VAL attribute, which in Ada 83 was described as taking ``any integer type.'' In Ada 9X, we can define its formal type as being root_integer'CLASS, and rely on the general class-wide matching rules (see MS-3.3.3) to allow operands to be of any integer type. We have defined a name for this class-wide type. This allows the user to define integer-class-wide operations. We have also proposed additional operations in TEXT_IO to allow integer input/output without using generic instantiation (see MS-14.3.7,8), by specifying the integer class-wide type for the formal parameter. In MS version 4.0, the name INT_CLASS was proposed for the integer class-wide type. Reviewer comments have suggested that a user might not realize that values of this type are manipulated at run-time using the longest (and presumably slowest) integer arithmetic. Therefore, we have more recently suggested the name ROOT_INTEGER_CLASS instead, to emphasize that root-integer arithmetic will be used. We have specified minimum ranges for INTEGER and LONG_INTEGER (as well as minimum digits for FLOAT and LONG_FLOAT -- see MS-3.5.6) to provide some portability to programs that use such types. This will also improve interoperability with ANSI C, which has similar conventions. S.3.5.5. Operations of Discrete Types S.3.5.6. Real Types We are proposing to simplify the discussion of real types in the main part of the reference manual, and instead provide more details relevant to numeric error analysis in a separate annex section (see MS-L, Numerics Annex). As part of the simplification, we are also eliminating the concept of accuracy subtypes. The rationale for this is provided above in the section on Subtype Declarations. Note that just because these details are described in a Specialized Needs Annex does not make them optional; each Annex defines what is optional. To further simplify the description of real numbers, and to improve the correspondence between the real type attributes and the machine attributes, the attributes will be redefined so that they more closely correspond to the capabilities of the machine. See the rationale for the Numerics Annex for an explanation of these changes. As with integer types, we have defined a class-wide real type, root_real'CLASS. This allows real-class-wide operations like literals and named numbers to be incorporated into the general class-wide matching rules. In MS version 4.0 we suggested the name REAL_CLASS for this real class-wide type, but as mentioned above, this could lead to some user misconception about which arithmetic would be used for run-time manipulation of values of such a type. Therefore, we have more recently suggested that the name ROOT_REAL_CLASS be used. As in Ada 83, run-time calculations using root_real'CLASS (universal_real in Ada 83) use the most precise floating point type. S.3.5.7. Floating Point Types See the rationale on Subtype Declarations for why we are removing subtype accuracy constraints. S.3.5.8. Operations of Floating Point Types This section was mislabeled in MS version 4.0. It should have been labeled ``Fixed Point Types.'' S.3.5.9. Fixed Point Types In Ada 83, there was some confusion between the concepts of delta and small. In Ada 9X, we are requiring that the default choice of small be such that the delta is represented exactly. In other words, the delta and the small must be the same, or the delta must be an integral multiple of the small. Since publishing MS version 4.0, we have concluded that there is no reason to allow the delta and the default choice of small to differ. Hence, we are now proposing (see MS-L, Numerics Annex) that delta and small be the same, by default. S.3.5.10. Operations of Fixed Point Types S.3.6. Composite Types [new] For Ada 9X, we are introducing a new section on Composite Types, where discriminants and other general properties of composite types will be described. This is partly a presentation issue and partly reflects the generalization of the semantics to allow discriminants on all composite types. As a simple generalization to Ada 83, we have allowed both variables and constants of an unconstrained composite type, without defaults for all discriminants, to be declared, so long as an initial value is specified. In Ada 83, only initialized constants of such an unconstrained composite type could be declared. However, the implementation considerations are essentially identical for constants and variables, so eliminating the restriction against variables imposes no extra implementation burden, and simplifies the model. Here is an example of use: if ANSWER /= CORRECT_ANSWER then declare IMAGE : STRING := ANSWER_ENUM'IMAGE(CORRECT_ANSWER); begin SET_TO_LOWER_CASE(IMAGE); PUT_LINE("The correct answer is " & IMAGE & '.'); end; end if; Allowing composite variables without a specified constraint to be declared, if initialized, is particularly important for tagged class-wide types, and generic formal types with formal discriminant part (<>), as such types have an unknown set of discriminants. S.3.6.1. Discriminants [originally 3.7.1] As part of the generalization of the type model, we have proposed that discriminants be allowed on any composite type, and that the type of a discriminant may be any non-limited type. Discriminants are the primary means for parameterizing a type in Ada, and therefore we have tried to make them as general as possible. Task and protected record types in particular benefit from discriminants acting as more general type parameters. In Ada 83, an instance of a task type must go through an initial rendezvous to get parameters to control its execution. In Ada 9X, the parameters may be supplied as discriminant values at the task object declaration, eliminating the need for the extra rendezvous. Variables introduced in the declarative part of the task body also can depend on the task type parameters/discriminants, as can the expression defining the initial priority of the task via a PRIORITY pragma. Discriminants in Ada 83 were restricted to being of a discrete type. This was due to their originally being used only in index and discriminant constraints in component declarations. However, even in Ada 83 their use was generalized to allow them to be used in default initial expressions for record components. Therefore, we have relaxed the restrictions on the type for a discriminant, but we have retained the ``by copy'' semantic requirement, effectively precluding limited types as discriminants. In addition to allowing previously declared non-limited types as discriminants, we have also proposed that an access discriminant of a general access type may be defined, using the syntax of an access parameter definition (see MS-3.9). Access discriminants provide several important capabilities. Because they impose minimal scope checking restrictions, an access discriminant may be initialized to reference an enclosing object (see MS-3.8.2), or to reference another object in the same scope. When an object might be on multiple linked lists, it is typical that one link points to the next link. However, it is essential to be able to also gain reference to the object enclosing the link as well. With access discriminants, this reference from a component that is a link on a chain, to the enclosing object, can be initialized as part of the default initialization of the link component. Another common use for a reference from one object to another is when one object acts as a ``cursor'' or ``iterator'' over another object. For example: type SET is limited private; -- a set of ITEM_TYPE procedure ADD_ITEM(TO : in out SET; ITEM : ITEM_TYPE); . . . type SET_ITERATOR(OVER : access SET) is limited private; -- an iterator over a SET; procedure ITERATE(ITERATOR : in out SET_ITERATOR; OVER : access SET); -- procedure to start an iteration over a SET procedure GET_NEXT(ITERATOR : in out SET_ITERATOR; ITEM : out ITEM_TYPE); -- procedure to get next item in the iteration function MORE(ITERATOR : SET_ITERATOR) return BOOLEAN; -- function returns true if more items left Without some support for a reference from one object to another possibly local object, this kind of iteration cannot be implemented without using generics. However, with generics, the iterator is not an object of some type, but rather is an instantiation of the generic. This limits the use of the iterator, since it cannot be passed around as a parameter. When an iterator is an object, it can be composed with other operations much more easily. For example, one could have a (tagged) class of iterators over ITEM_TYPE. One could then write a general ``apply'' operation that took an iterator object and a function and applied it to each item produced by the iteration. If an iterator is a generic instance, this is much more cumbersome. Discriminants on array types are particularly useful. The low bound is generally the same across all objects of a given array type. Nevertheless, in Ada 83, any formally unconstrained array parameter must include a run-time specification of its low bound, and this must be used in all indexing into the array. This introduces unnecessary overhead, while being a source of run-time errors when the unusual parameter with a low bound other than one does show up. By constraining the low bound to a specific value (typically one), and using the discriminant to set the high bound for the array, the efficiency is improved and the possibility for intermittent run-time error is eliminated. For example: type VECTOR(LENGTH : POSITIVE) is array(1..LENGTH) of INTEGER; A formal parameter V of this type only needs a single discriminant (LENGTH) to be passed along with the array value. Furthermore, a reference to the first element of the vector explicitly as V(1) will always work, because V'FIRST is always one, and because null vectors have been eliminated as a possibility due to the length being POSITIVE. With an integer array type defined without using discriminants, all operations would have to be sensitive to the possibility of a null vector, or a vector that did not start at one, which could make the operations significantly less efficient. Of course, the example type could be changed to specify the subtype of LENGTH as NATURAL instead of POSITIVE, allowing for null VECTORs if appropriate. Another useful ramification of allowing discriminants on array types is that bounded unconstrained array objects may be declared. In MS-14.3.6, we include an example of a type VAR_STRING with one discriminant Length, of subtype STRING_LENGTH, having a default of zero and a maximum of 255. If we then declare an unconstrained object of this type: VS : VAR_STRING; -- see MS-3.6.1(33) we have a string object VS that can be assigned strings of any length from zero to 255: VS := "abc"; VS := ""; VS := "This is a test."; VS := VS(1..VS.Length-1) & " of variable-length strings."; This capability should significantly enhance Ada's support for writing simple string-oriented programs. We have also proposed that a type derived from a composite type may specify a new set of discriminants. For untagged types, these new discriminants are not considered an extension of the original type, but rather act as renamings or constraints on the original discriminants (or bounds). As such, these discriminants must be used to specify the value of one of the original discriminants (or bounds) of the parent type. The new discriminant is tightly linked to the parent's discriminant or bound it specifies, since on conversion from the parent type, the new discriminant takes its value from that discriminant or bound (presuming CONSTRAINT_ERROR is not raised). For a tagged type, a new discriminant need not be used to specify a parent discriminant or bound. Instead it may represent a new component of the type, requiring additional storage in the representation of values of the derived type. A derived type with such a discriminant is considered a discriminant extension of the parent type. Such a discriminant may be used to constrain or initialize new components added as part of the derived type definition, or it may just represent some additional piece of information, which can be set at object creation time. It is important to note that all of these various new capabilities stem from the single generalization of allowing discriminants to act as general type parameters for composite types. S.3.6.2. Discriminant Constraints [originally 3.7.2] S.3.7. Array Types [originally 3.6] S.3.7.1. Index Constraints and Discrete Ranges [originally 3.6.1] In Ada 83, a discrete range like ``-5 .. 5'' was not legal, because it was considered ambiguous, even though ``1 .. 5'' was allowed. For Ada 9X, a discrete range with both bounds interpretable as root_integer or root_integer'CLASS is always treated as a subrange of STANDARD.INTEGER, making ``-5 .. 5'' legal, both as the bounds for an array type, and in a for loop statement. S.3.7.2. Operations of Array Types [originally 3.6.2] S.3.7.3. String Types [originally 3.6.3] S.3.8. Record Types [originally 3.7] A record type may be specified as tagged or limited in its definition. This makes record types consistent with private types, and allows a tagged record type to be declared limited even if none of its components are limited. This is important because only limited types may be extended with components that are limited (see MS-3.4.1). S.3.8.1. Variant Parts [originally 3.7.3] Although we have generalized discriminants so that a discriminant may be of any elementary type, we still require that a discriminant used to select a variant part be discrete. S.3.8.2. Record Extension [new] A derived type is a record extension if it includes a record extension part, which has the same syntax as a normal record type definition preceded by the reserved word with. For example: type LABELLED_WINDOW(LENGTH : NATURAL) is new WINDOW with record LABEL : STRING(1..LENGTH); end record; Record extension is the fundamental type extension (type inheritance) mechanism in Ada 9X. A private extension must be defined in terms of a record extension (see MS-7.4.2). The new discriminants in a discriminant extension are normally used to control the new components defined in the record extension part (as illustrated in the above example). Record extension is a natural evolution of the Ada 83 concept of derived types. From an implementation perspective, it is relatively straightforward, since the new components may all be simply added at the end of the record, after the components inherited from the parent type. Because the parent type must itself be tagged, the representation of the parent type can be specifically designed so that it is efficient to address such new components. We considered having other kinds of type extension, including enumeration type extension, task type extension, and protected record type extension. However, none of these seemed clearly as useful as record extension, and all introduced additional implementation complexities. In any case, the automatic assignment of tags to type extensions lessens the need for enumeration types, and the added flexibility associated with access-to-subprogram and dispatching operations makes it less critical to allow task bodies to be extended. Type extension of protected records is the most interesting possibility. However, certain implementation approaches may not easily support extension of the set of protected operations, or the changing of the barrier expressions. The final decision about whether the benefit of extending protected records is worth the implementation burden may have to await more user and implementor feedback. Type extension is only permitted if the parent type is tagged. Originally we considered allowing any record or private type to be extended, but this introduced additional complexity, particularly inside generics, where a ``private'' type might in fact be any kind of type. Furthermore, extending an untagged type breaks the general model that a class-wide type can faithfully represent any value in the class. An object of an untagged class-wide type does not have any provision for holding a value of a type extension, since it lacks a run-time type tag to describe the value. S.3.8.3. Operations of Record Types [originally 3.7.4] S.3.9. Access Types [originally 3.8] For Ada 9X, access types have been generalized so that they may be used to designate subprograms, and declared objects. Access types that may designate declared objects are called general access types, as distinguished from pool-specific access types, which correspond to those provided by Ada 83. General access types have the reserved word all or constant in their definition. We originally considered allowing any (object) access type to designate a declared object, but this would have forced all access types to be represented as full addresses. By distinguishing general access types from pool-specific access types, we preserve the possibility of optimizing the representation of a pool-specific access type, by taking advantage of its limited storage-pool size. A value of a general access type declared with the reserved word all can only designate variables (not constants), and may be used to update the designated object. If the reserved word constant is used, then access values may designate constants, as well as variables. An object designated by an access-to-constant value may not be updated via the access value. An allocator for an access-to-constant type, if used to initialize a library-level constant access object, might be evaluated prior to run-time. Such an allocator might statically reserve storage in a read-only part of the address space. For Ada 9X, we have generalized the concept of access collection to the concept of a storage pool. Every access-to-object type has an associated storage pool, which may be shared with other access types. In particular, any derivative of an access type shares the same storage pool as the parent access type. An allocator for an access type allocates storage from the associated storage pool. In the Systems Programming Annex, we define mechanisms for defining a storage pool type, and associating an object of a storage pool type with a particular access type. Alternatively, a bounded storage pool may be requested by specifying the STORAGE_SIZE attribute for an access type, as in Ada 83. In the absence of a specification of either the STORAGE_POOL or STORAGE_SIZE attribute of an access type, the implementation chooses an appropriate storage pool for the type. Pool-specific access values never point outside of their storage pool (in the absence of an unchecked conversion). On the other hand, general access values may be assigned to point to any aliased object of an appropriate type and scope, either through the use of the ACCESS attribute or explicit access type conversion (see MS-3.9.2). The storage pool concept makes explicit the notion of a ``heap,'' and when combined with the ability to specify a STORAGE_POOL object for an access type (see MS-G, Systems Programming Annex), gives the user better control over dynamic allocation. Access types are used heavily in object-oriented applications. To enable the use of access types with the run-time dispatching provided for the primitive operations of tagged types, we have proposed a new kind of in parameter, called an access parameter. An access parameter is matched by an actual operand of any access type with the same designated type (see MS-3.3.3). Furthermore, if a subprogram has an access parameter with designated type T, and the subprogram is defined in the same list of declarations as the type T, then the subprogram is a primitive operation of T, and dispatches on the tag of the object designated by the access parameter. Inside the subprogram, an access parameter is of an anonymous general access type, and must either be dereferenced or explicitly converted on each use, or passed to another operation as an access parameter. Here is an example of a heterogeneous linked list using tagged types and access parameters: -- Define a root NODE type that can be linked onto a list type NODE; type NODE_PTR is access all NODE'CLASS; -- class-wide general access type type NODE is tagged record NEXT : NODE_PTR; end record; -- Define a list header type, which supports PREPEND and APPEND -- The list is heterogeneous, because NODE_PTR is an access to the -- class-wide type for the class rooted at NODE. type NODE_LIST is record FIRST : NODE_PTR; LAST : NODE_PTR; end record; . . . procedure PREPEND(LIST : in out NODE_LIST; PTR : access NODE) is -- Define the PREPEND operation as a primitive operation of NODE begin -- Put on front of list. PTR.NEXT := LIST.FIRST; LIST.FIRST := NODE_PTR(PTR); if PTR.NEXT = null then -- Identify as last item as well. LIST.LAST := NODE_PTR(PTR); end if; -- The object is now the first on the list. -- PTR.all'TAG is not necessarily NODE'TAG. -- The tag is instead the tag of the specific -- type specified in the declaration or allocator -- that created the object designated by PTR. end PREPEND; . . . type OBJECT is new NODE with -- Define an extension of node with useful INFO. record INFO : SOME_TYPE; end record; -- PREPEND is inherited from NODE type OBJ_PTR is access OBJECT; -- Define an access type for use on objects only . . . SOME_LIST : NODE_LIST; PTR : OBJ_PTR := new OBJECT'(INFO=>SOME_VALUE, NEXT=>null); -- Dynamically allocate an OBJECT . . . PREPEND(SOME_LIST, PTR); -- This calls the inherited PREPEND. -- A conversion is performed on the actual parameter -- before executing the body of PREPEND (see RM-3.4(14.) The above example illustrates how access types may be used in conjunction with type extension, to create heterogeneous linked data structures. Furthermore, it illustrates the use of access type conversion within a primitive operation PREPEND that actually links an object into a linked data structure. Note that an alternative approach would make PREPEND a class-wide operation for the class rooted at NODE, rather than a primitive operation of each type in the class. A class-wide operation takes a parameter of a class-wide type, or an access-to-class-wide type, and implements the operation for any type in the class, perhaps calling other class-wide operations or dispatching operations to do so. A class-wide operation would make sense since the implementation of PREPEND is unlikely to be changed in any derivative. A class-wide version of PREPEND would have the following parameter profile: procedure PREPEND(LIST : in out NODE_LIST; PTR : access NODE'CLASS); Whether to use a primitive (dispatching) operation or a class-wide operation is determined by whether or not the implementation of the operation depends on the specific type of the operand. In this case, it seems likely that the algorithm for PREPEND would be independent of the specific derivative type, so a class-wide operation would be appropriate. An additional advantage of operations that use an access parameter rather than a parameter of a specified access type, is that the overhead of the null pointer check is pushed to the caller, where it might be eliminatable by the optimizer. S.3.9.1. Incomplete Type Declarations [originally 3.8.1] S.3.9.2. Operations of Access Types [originally 3.8.2] A new attribute designator, ACCESS, has been defined for creating an access value designating a subprogram or object specified in the prefix. For example: A := OBJ'ACCESS; -- Point to a declared object B := SUBP'ACCESS; -- Point to a subprogram Full type checking is performed as part of interpreting the ACCESS attribute. An additional check is performed to ensure that the scope of the designated subprogram or object will not end before that of the access type, minimizing the possibility of dangling references. For subprograms, this scope check ensures that up-level references from within the subprogram will be meaningful when the subprogram is ultimately called via the access value. For objects, this scope check prevents the access value from being stored in a global and then exiting the scope where the designated object is declared. For subprogram'ACCESS, the designated subprogram must have formal parameter/result subtypes and a calling convention that statically match those of the access type. This allows the compiler to emit the correct constraint checks, and use the correct parameter passing conventions when calling via an access-to-subprogram value, without knowing statically what subprogram is being called. For object'ACCESS, the designated object must be marked aliased in its declaration, and must have a subtype that statically matches the designated subtype of the access type. By marking an object aliased, the compiler is informed that the representation of the object should correspond to that used for objects created by an allocator. In addition, the optimizer is informed that this object is likely accessible via one or more access values, and therefore its value might change as a result of an update via an access value. Indirect access to a subprogram is extremely useful for table-driven programming, using, for example, a state machine model. It is also useful for installing call-backs in a separate subsystem (like the X Window System). Finally, it provides an alternative to generic instantiation, allowing a non-generic parameter to be a pointer to a subprogram, such as for applying an operation to every element of a list, or integrating a function using a numerical integration algorithm. Indirect access to declared objects is useful for avoiding dynamic allocation, while still allowing objects to be inserted into linked data structures. This is particularly useful for systems requiring link-time elaboration of large tables, which may use levels of indirection in their representation. Such access types are also convenient for returning a reference to a large global object from a function, allowing the object to be updated through the returned reference if desired. Finally, rather than relying on allocators, it is sometimes appropriate to use a statically allocated array of objects, managed explicitly by the application. However, it may still be more convenient to reference components of the array using access values. By declaring the array components as aliased, the ACCESS attribute may be used to produce an access value designating a particular component. For a tagged type T and an aliased object X of type T, X'ACCESS and new T are overloaded on all access-to-T, on all access-to-T'CLASS, and on all other access-to-class-wide types that include T. These overloadings on access-to- class-wide types allow allocators and the ACCESS attribute to be used conveniently when calling class-wide operations, or building heterogeneous linked data structures. Overload resolution of the ACCESS attribute applied to an overloaded subprogram name represents a new situation in Ada. In Ada 83, the prefix of an attribute was required to be resolvable without context. However, for the ACCESS attribute to be useful on overloaded subprograms, it was necessary to allow the ACCESS attribute to use context to resolve the prefix. Therefore, if the prefix of ACCESS is overloaded, then context is used to determine the specific access-to-subprogram type, and then the parameter and result type profile associated with that access type is used to resolve the prefix. The ACCESS attribute may be applied to a constant aliased object. In that case, after resolving the access type, a check is made that it is an access-to-constant type. On use of an access-to-constant value, the designated object cannot be updated. These two rules allow safe use of pointers to constant objects, including objects statically allocated to a read-only part of the address space (e.g. in ROM). S.3.10. Declarative Parts [originally 3.9] Ada 83 has a restriction that type, subtype, and object declarations may not come after bodies or body stubs within a declarative part. This restriction is being lifted for Ada 9X. The original restriction reflected Ada's Pascal heritage, where the ordering restrictions between declarations are even more restrictive. However, in retrospect, the restriction seems somewhat arbitrary, and forces the separation of declarative items that might more naturally be grouped together, particularly in a package body declarative part. By removing this restriction, it becomes legal to move local variable declarations of a subprogram body to the end of its declarative part. This ensures that such variables are not accessible for up-level references from nested subprograms declared in the same declarative part. By so doing, it makes it easier for a compiler to allocate such variables to hardware registers, rather than having to keep them in memory locations to support possible up-level references. Having removed this restriction, it is necessary to rely more heavily on the Ada 83 rule (see RM 13.1(5-7)) that the representation for a type is ``forced'' by the appearance of a body (including a body stub). This rule precludes the separation of a representation clause from its associated declaration by a body. In Ada 83, this requirement was a ramification of the syntax, since representation clauses were not allowed to follow bodies syntactically. In Ada 9X, the requirement becomes more relevant, since representation clauses are syntactically allowed to appear anywhere in a declarative part. S.4. Ada 9X Names and Expressions S.4.1. Names S.4.1.1. Indexed Components S.4.1.2. Slices S.4.1.3. Selected Components S.4.1.4. Attributes S.4.2. Literals S.4.3. Aggregates S.4.3.1. Record Aggregates Record aggregates are only permitted for a type extension if both the extension part and the parent part are fully visible. This corresponds to the principle that if part of a type is private, then it must be assumed to have an unknown set of components in that part. S.4.3.2. Array Aggregates Ada 83 has a rule that determines where an array aggregate with named choices and an others choice is permitted (see RM 4.3.2(6)). There are a related set of rules that govern where implicit array subtype conversion (``sliding'') is permitted for an array value (see RM 3.2.1(16), RM 5.2.1(1)). These rules were constructed to ensure that named-with-others array aggregates and array sliding were not both permitted in the same context. However, the lack of array sliding in certain contexts can result in unanticipated CONSTRAINT_ERRORs because the bounds don't match the applicable constraint. For Ada 9X, we are relaxing the restrictions on both array sliding and named-with-others array aggregates, so that both are permitted in all contexts where an array aggregate with just an others choice is legal in Ada 83. This corresponds to all situations where an expression of the array type is permitted, and there is an applicable index constraint (see RM 4.3.2(4-8)). This ensures that sliding takes place as necessary to avoid CONSTRAINT_ERRORs, and simplifies the rules on array aggregates with an others choice. The original Ada 83 restrictions were related to the possible ambiguity between determining the bounds of an aggregate, and sliding. For Ada 9X, we resolve this ambiguity by stipulating that no sliding takes place on an array aggregate with an others choice. The applicable index constraint determines the bounds of the aggregate. Array aggregates are useful both for defining a static table with compile-time known values, and for dynamically constructing an array from computed components. In both circumstances, it is often useful to be able to refer to the value of the array index when defining the value for an array component. For Ada 9X, we allow a name to be specified for the array index, to allow the expression defining a component of an array aggregate to depend on the component's index. Here is an example that defines a static table for mapping characters from lower case to upper case, using an array aggregate with a named array index. Example of named array index in an aggregate: -- Build static table mapping lower case to upper case CASE_OFFSET : constant := CHARACTER'POS('a') - CHARACTER'POS('A'); type CHAR_MAP is array(CHARACTER) of CHARACTER; UPPER_CASE_MAP : CHAR_MAP := CHAR_MAP'( for LC in 'a'..'z' => -- LC is name for array index CHARACTER'VAL(CHARACTER'POS(LC) - CASE_OFFSET), for UC in others => UC); -- UC is name for array index procedure CONVERT_TO_UPPER_CASE(S : in out STRING) is -- This procedure uses the static mapping to convert to upper case begin for I in S'RANGE loop S(I) := UPPER_CASE_MAP(S(I)); end loop; end CONVERT_TO_UPPER_CASE; Here is an example that uses a named array index to select at run-time the diagonal from a square matrix: -- Declare a type for vectors of floats type FLOAT_VECTOR is array(POSITIVE range <>) of FLOAT; -- Declare an operation for summing a vector function SUM_ARRAY(ARR : FLOAT_VECTOR) return FLOAT; . . . -- Declare a square matrix MAT : array(1..10, 1..10) of FLOAT := ( ... ); -- Sum the diagonal of the square matrix DIAGONAL : FLOAT := SUM_ARRAY((for I in 1..10 => MAT(I,I))); For numerically intensive applications, we considered standardizing a specialized pragma ARRAY_SECTION to allow an aggregate, like the one in the example, which represents a cross section of another array, to be represented using a descriptor rather than by copy. This proposal has since been dropped, though an implementation might still choose to support it or something similar. S.4.4. Expressions In Ada 83 a formal parameter of mode out could not be read, even after being initialized within a procedure. This forced certain algorithms to include a local variable just to accumulate a result, which then was assigned to the out parameter. Introducing such a local variable is error prone, because the final assignment may be mistakenly omitted. Similarly, if an out parameter is passed to a second procedure to be filled in, the value returned cannot be checked prior to returning from the first procedure. For Ada 9X, we are removing the restrictions on the use of out parameters. Specifying that a formal parameter is of mode out indicates that the caller need not initialize it prior to the call. However, within the procedure, once the parameter has been initialized, it may be read and updated like any other variable. As with a normal variable, it is an error to depend on the value of an out parameter prior to its being initialized. The added simplicity and flexibility provided by removing the restrictions on reading an out parameter outweigh the possible loss of safety. Furthermore, as described in MS-6.2(3), if any subcomponent of the actual parameter has default initialization, then the value of that subcomponent will be preserved if it is not assigned within the procedure. S.4.5. Operators and Expression Evaluation S.4.5.1. Logical Operators and Short-circuit Control Forms S.4.5.2. Relational Operators and Membership Tests For Ada 9X, the predefined equality operators and the membership tests are generalized to apply to tagged class-wide types (see MS-3.4.3). Like other predefined operations on such types, the implementation will depend on the particular type tags of the operands. Unlike normal dispatching operations, however, CONSTRAINT_ERROR is not raised if the tags of the operands do not match. For equality, tag mismatch is treated as inequality. Only if the tags match is a dispatch then performed to the type-specific equality checking operation. This approach allows a program to safely compare two values of a class-wide tagged type for equality, without first checking that their tags match. The fact that no exception is raised in such an equality check is consistent with the other predefined relational operators, as noted in RM 4.5.2(12). For membership, the tag of the value of the simple expression and the tag of the type specified by the type mark are compared to determine whether the value is a member of the type denoted by the type mark. If the type mark in a membership test identifies a class-wide tagged type, then the membership test tests whether the tag of the value identifies a specific type included in the class-wide type. If the type mark in a membership test identifies a specific tagged type, then the membership test tests whether the tag of the value equals the tag of that type. In any case, to be considered a member, the value must satisfy any constraints associated with the type mark. The rules for membership tests on class-wide tagged types are constructed so that certain simple type-specific behavior may be performed in a class-wide operation, without the need to declare and define a new primitive operation on all types within the class. For example, given an EXPRESSION_NODE type with a derived BINARY_OPERATOR type, one could define the following operation DISPLAY on the access to class-wide type NODE_PTR: procedure DISPLAY(EXPR : NODE_PTR; PREC : POSITIVE := 1) is -- Display expr, parenthesized if necessary begin if EXPR.all in BINARY_OPERATOR'CLASS then -- Handle the binary operator subclass declare BINOP : constant BIN_OP_PTR := BIN_OP_PTR(EXPR); -- Convert parameter to ptr to BINARY_OPERATOR -- to gain access to its subclass operations begin if PRECEDENCE(BINOP.all) < PREC then -- Parenthesize if lower precedence PUT('('); end if; -- Display left, op, right, passing down precedence DISPLAY(BINOP.LEFT, PRECEDENCE(BINOP)); PUT(SYMBOL(BINOP)); DISPLAY(BINOP.RIGHT, PRECEDENCE(BINOP)); if PRECEDENCE(BINOP) < PREC then -- Closing parenthesis if necessary PUT(')'); end if; end; else -- Handle the other kinds of expressions . . . end if; end DISPLAY; An alternative, more ``object-oriented'' approach would be to define a separate DISPLAY primitive operation for each distinct type within the class. S.4.5.3. Binary Adding Operators For Ada 9X, we are revising the definition of catenation so that its bounds may be more efficiently determined. In Ada 83, significant overhead is incurred in handling operands that might be null arrays. In Ada 9X, the bounds are determined independent of whether the left operand is null. In particular, the low bound of the result of a catenate is always the same as the low bound of the left operand. The high bound is calculated from the low bound and the total length. In Ada 83, a null left operand meant that the low bound was taken from the right operand. For Ada 9X, the left operand always determines the low bound of the result. This significantly simplifies the sequence of instructions required to perform a catenation, and generally produces a more predictable result. For example: function REMOVE_CHAR(STR : STRING; INDEX : POSITIVE) return STRING is -- This function removes one character from -- a string and returns the shortened result. begin return STR(STR'FIRST..INDEX-1) & STR(INDEX+1..STR'LAST); end REMOVE_CHAR; Using Ada 83 rules, the low bound of the result of REMOVE_CHAR would either be STR'FIRST, or STR'FIRST+1 depending on the value of INDEX. Using Ada 9X rules, the low bound of the result of REMOVE_CHAR will always be STR'FIRST, simplifying its use. This simplification in the rules for catenate also provides some relief for the anomaly in Ada 83 that caused catenate for a type declared by a constrained array definition to generally raise CONSTRAINT_ERROR. For example, in Ada 83, the following raises CONSTRAINT_ERROR, while in Ada 9X it will produce the desired result: X : array(1..10) of CHARACTER; begin X := "" & X(6..10) & X(1..5); In Ada 83, the null string is essentially ignored by the catenate, and the bounds of the result of the catenate are 6..15, which causes a constraint error since 15 is greater than the high bound of the index subtype. In Ada 9X, the low bound of the null string is used to determine the low bound of the result, so the bounds of the result are 1..10, as required. S.4.5.4. Unary Adding Operators S.4.5.5. Multiplying Operators The ``extra'' multiplying operators of the Ada 83 universal numeric types are redefined in Ada 9X in terms of the root numeric types. Note that we inadvertantly omitted the division operator for fixed point types from MS-4.5.5. Its signature is: function "/"(LEFT, RIGHT : root_fixed'CLASS) return precise_fixed; By using root numeric types, the special Ada 83 rules regarding convertible universal operands (see RM 8.6(15)) may be eliminated. Instead, the distinction between convertible and non-convertible universal operands corresponds directly to the distinction between the class-wide and specific root numeric types. The operators of the root numeric types return specific root numeric types, and hence their result is not class-wide (not ``implicitly convertible'' using Ada 83 terminology). The general class-wide matching rules (MS-3.3.3) ensure that these operators accept operands of the root class-wide types, so they may be used on literals and named numbers. Defining the fixed-fixed multiplying operators in terms of a root_fixed type has a couple of advantages. First of all, it eliminates the somewhat mysterious situation in Ada 83 where multiplying operators between any two fixed point types magically appear in package STANDARD. In Ada 9X, we need only have one "*" and one "/" operator for fixed-fixed multiply and divide. Because the formal parameters are of the class-wide type root_fixed'CLASS, they are matched by any fixed point type. Furthermore, as a second advantage, the formal root_fixed'CLASS parameters are also matched by root_real'CLASS according to MS-3.3.3, and therefore literals and named numbers are usable in fixed-fixed multiplying operators. This eliminates a confusing Ada 83 requirement to explicitly convert a real literal or named number before using it in a fixed-fixed multiplying operator. It is particularly pleasant that this additional capability comes ``for free,'' with no need for a special rule to allow it. S.4.5.6. Highest Precedence Operators S.4.6. Type Conversions Because Ada 9X supports type extension (see MS-3.4.2), type conversion becomes a more complex operation. In Ada 83, type conversion involved only a possible representation change and a possible constraint check. If the conversion succeeded, no components were lost or added, and the conversion was always reversible. For Ada 9X, when converting to an extension, the additional components must be defined with component associations in the type conversion. When converting from an extension, the additional components are lost. Conversion is therefore not always reversible. In a subprogram call, an actual parameter of mode out or in out may still be in the form of a type conversion, even when the operand is an extension of the target type. In that case, the extension part is unaffected by the call (see MS-6.4.1). The syntax proposed for converting to an extension is a cross between the syntax for simple type conversion, and the syntax for named record aggregates. Names are required for any newly specified components, to avoid confusion that might result when converting across a hierarchy of various type extensions. When converting to a direct descendant, or a direct ancestor type, the conversion involves only strictly extension or strictly truncation. However, when converting across a hierarchy, the conversion is defined by first truncating to the nearest common ancestor type, and then extending to the target type. Converting from a tagged class-wide type to a specific type may drop components, but not add them. A tag check verifies that the type identified by the tag of the operand matches that of the target type, or is a derivative of it. As described in S.3.9, we are also allowing conversion between general access types to better support programming with access types designating tagged types. Essentially, an access type conversion is permitted if access values of the target type may ``safely'' designate the object designated by the operand access value. See MS-4.6(27-34). As explained in S.4.3.2, we are generalizing implicit subtype conversions on arrays (``sliding'') to apply in more circumstances. These new rules should minimize the times when an unexpected CONSTRAINT_ERROR arises when the length of the array value is appropriate, but the upper and lower bounds don't match the applicable index constraint. In effect, we are treating the bounds as properties of array objects rather than array values. Array values have a length for each dimension, but the bounds may be freely readjusted to fit the context. Note that sliding is not automatically performed for out and in out actual parameters, as these are considered objects rather than values. S.4.7. Qualified Expressions Qualified expressions are generalized to handle class-wide types. In general, a qualified expression is treated just like a call on a non-overloaded identity function where the formal parameter subtype (and the result subtype) is the subtype specified in the qualified expression. S.4.8. Allocators S.4.9. Static Expressions and Static Subtypes In Ada 83, static expressions were limited to predefined operators applied to static operands, static attributes, or a qualified static expression. For Ada 9X, we are generalizing the rules so that other basic operations may be used in static expressions, as well as attributes and discriminants of statically constrained objects. By allowing more constructs in static expressions, the programmer has more freedom in contexts where static values are required. In addition, we ensure more uniformity in what expressions are evaluated at compile-time. Some compilers were aggressive in evaluating compile-time known expressions, while others only evaluated those expressions that were ``officially'' static. By shifting the definition of static to more closely correspond to compile-time-known, uniformity of efficiency is enhanced. In addition to generalizing the rules for static expressions, we have also required that all static evaluation be performed exactly. Although many compilers already perform all compile-time arithmetic with arbitrary precision, this rule will provide more predictability for the value of a static expression. Note that the exact static value must still be converted to a machine manipulable representation when combined in an expression with non-static values. This conversion inevitably introduces some implementation- dependence. S.4.9.1. Statically Matching Constraints and Subtypes [new] For Ada 9X, we are introducing access-to-subprogram types (see MS-3.10). When calling through a value of such an access type, the actual parameters must be prepared and the call must be performed given only the address (and perhaps a static link) for the target subprogram. The way parameters are passed, the constraints that need to be checked, the way the result is returned, and any other calling conventions must be determined completely knowing only the definition of the access-to-subprogram type. We have defined a new kind of subprogram conformance, called subtype conformance (see MS-6.3.1). This kind of conformance is required between the parameter and result specifications given in an access-to-subprogram type definition, and the specification of a potential designated subprogram. Subtype conformance is based on a static match between the subtypes of corresponding parameters and the result, if any. A static subtype match is required for access-to-subprogram matching because the static properties of a formal parameter are presumably the ones that govern the parameter passing mechanism selected for it. For example, if two array subtypes happen to impose the same index constraint, but one is known at compile time to be (1..4), while the other is not known at compile time, then the first might be passed by copy while the second would be passed by reference. Section 4.9.1 of the Mapping Specification defines the static matching rules for constraints and subtypes. The rules are defined for both static and dynamic constraints. When the constraints are static, they must be equivalent. When the constraints are dynamic, they must originate from the same subtype indication (thereby assuring that they are equivalent). If one constraint is static and one is dynamic, then they never statically match. To minimize run-time checks, the subtype of a discriminant specified for an array type or a derived type must be statically compatible with the subtype of the underlying bound or discriminant. This means that if a value satisfies the requirements of an ``outer'' discriminant, it necessarily satisfies the requirements of the ``inner'' discriminant defined by it, without any need for a run-time constraint check. This simplifies object initialization, as well as type conversion, since in both cases, only the outermost discriminant's subtype need be checked. S.5. Ada 9X Statements No syntax changes are proposed for the statements described in Chapter 5. The rules for assignment statements are generalized to handle class-wide types, by requiring that the type of the right hand side match the root of the left hand side. This implies that assignment is not a class-wide operation, but rather is a normal primitive operation with both operands expecting the same specific type. The revised rules proposed in MS-3.7.1 for determining the type of a discrete range eliminate an Ada 83 anomaly relating to for loops (see S.3.7.1). S.6. Ada 9X Subprograms S.6.1. Subprogram Declarations We have proposed two syntax changes for subprogram specifications: - A subprogram may be specified as abstract using the notation is <>. An abstract subprogram has no implementation, and may not be called directly or indirectly. See S.3.4.1 for a discussion of abstract subprograms. - A formal in parameter may be specified with an access parameter definition. Such a parameter is of a general access type that is totally anonymous (has no namable subtypes), but that is convertible to other general access types with the same designated subtype. See MS-3.9. The primary purpose of access parameters is to support dispatching on access values (see S.3.3.3). S.6.2. Formal Parameter Modes In Ada 9X it is not erroneous to depend on the parameter passing mechanism (by-reference vs. by-copy), though it is non portable. This is an example of reducing totally unpredictable behavior (see S.1.6). In Ada 83, out parameters were ``write-only'' variables. This restriction prevented reading an out parameter before it was initialized, but also made the legitimate use of an out parameter more awkward. As discussed in S.4.4, for Ada 9X, we are lifting the restriction against reading out parameters. By so doing, many of the special cases associated with out parameters are eliminated, including the restriction regarding their use with limited types (see RM 7.4.4(4)). In addition to allowing out parameters to be read, we are requiring that a subcomponent not become ``deinitialized'' by being passed as an out parameter. If a subcomponent of a type has default initialization, the value of the subcomponent must not be lost as a result of a subprogram call, when no assignment is made to the subcomponent. S.6.3. Subprogram Bodies For Ada 9X, we are allowing a subprogram body to be provided by a generic instantiation, or by renaming another subprogram. In both cases, the subprogram specification must be subtype conformant with the body used to implement it. Subtype conformance (MS-6.3.1(5)) is required because the caller sees only the subprogram specification, and therefore has prepared the parameters, performed constraint checks, and followed parameter passing conventions determined by the specification. S.6.3.1. Conformance Rules Several different rules exist in Ada 83 governing matching between subprogram specifications. For the purposes of hiding, only the types of the formal parameters and the result, if any, are relevant (see RM 8.3(15)). For renaming and generic instantiation, the modes must also match (RM 8.5(7), RM 12.3.6(1)). Between a specification and its body, syntactic equivalence is required (RM 6.3.1(5)). For Ada 9X, to support indirect access to subprograms (MS-3.10), and to support the implementation of a specification by a generic instantiation or renaming (MS-6.3), an intermediate level of matching is needed. This intermediate level requires static subtype matching (MS-4.9.1), but allows formal parameter names and defaults to differ. To improve presentation, for Ada 9X we are centralizing the description of these various levels of subprogram matching into a single section on Conformance Rules (MS-6.3.1). Each level of matching is given a name: Type conformance This is the matching that controls hiding. Mode conformance This is the matching required for renaming and generic subprogram parameters. Subtype conformance This is the matching required for indirect access to subprograms, and for specifying a body via renaming or generic instantiation. Full conformance This is the matching required between the declaration and body of a subprogram, and between multiple specifications of a discriminant part. In addition to centralizing these definitions, we are also proposing to relax the full conformance rules, to make them represent static semantic equivalence, rather than syntactic equivalence. This has the effect of eliminating certain anomalies (such as the non-transitivity of Ada 83 conformance), as well as being more natural for the programmer and easier to implement. S.6.3.2. Inline Expansion of Subprograms S.6.4. Subprogram Calls S.6.4.1. Parameter Associations For Ada 9X, we are defining by-copy parameter passing in terms of a subtype conversion and an assignment. This should minimize the number of special rules associated with parameter passing. For by-reference parameters, the formal parameter is considered a view of the actual. For in parameters, a subtype conversion is performed if necessary (to provide sliding). As in Ada 83, references to the formal parameter may refer to a local copy created at the time of the call, rather than directly to the actual parameter. On normal return, such a local copy must be copied back into the actual. S.6.4.2. Default Parameters S.6.5. Function Subprograms S.6.6. Parameter and Result Type Profile - Overloading of Subprograms S.6.7. Overloading of Operators In Ada 83, the "=" operator may only be explicitly defined for limited types (other than via a round-about method based on generic instantiation). This restriction was justified largely on methodological grounds. However, experience with Ada has illustrated several circumstances where it is very natural to provide a user-defined equality operator for non-limited types. For example, within a three-value logic abstraction, "=" should return either TRUE, FALSE, or UNKNOWN. For vector processing, it is natural to define a component-wise "=" operator for vectors, producing a vector of booleans as the result. In such cases, it is also important to be able to explicitly define "/=", since it is not the simple boolean complement of "=". For Ada 9X, we are allowing "=" to be treated like any other relational operator. The only remaining special aspect of "=" is that when the result type of a user-defined "=" operator is STANDARD.BOOLEAN, a complementary definition for "/=" is automatically provided. Explicit definitions for "/=" are also permitted, so long as the result type is not STANDARD.BOOLEAN. A "/=" operator with a result type of STANDARD.BOOLEAN may only become defined as an implicit side-effect of a definition for "=". S.7. Ada 9X Packages S.7.1. Package Structure S.7.2. Package Specifications and Declarations S.7.3. Package Bodies S.7.4. Private Type and Deferred Constant Declarations S.7.4.1. Private Types To allow for the extension of private types, the modifier tagged may be specified in a private type declaration. A tagged private type must be implemented with a tagged record type, or by deriving from some other tagged type. We considered allowing a tagged private type to be derived from an untagged type. However, this added potential implementation complexity because the parent type might not have a layout optimized for referencing components added in later extensions. There is a simple work-around, by implementing the tagged private type as a tagged record type, with a single component of the desired parent type. For Ada 9X, we consider a private type to be a composite type outside of the scope of its full type declaration. This is primarily a presentation issue. We have generalized discriminants so that a derived or array type declaration may include a discriminant part (see MS-3.6.1). Therefore, it is now permissible for a private type with discriminants to be implemented by a derived or array type declaration. This was not possible in Ada 83. In addition, a discriminant part may be provided in a task or protected record type declaration (see MS-9.1 and MS-9.5). Therefore, a limited private type with discriminants may be implemented as either a task or protected record type. S.7.4.2. Private Extension [new] A tagged (parent) type may be extended with a private extension part. This allows one type to visibly extend another, while keeping the names and types of the components in the extension part private to the package where the extension is defined. For each private extension in the visible part, a record extension must be defined in the private part. Note that the record extension part may be simply with record null; end record; If a private extension is implemented with such a null record extension, the effect is to prevent conversion from the parent type to the private extension (see MS-4.6(9)) outside the package defining the private extension. Conversion is disallowed even though the private extension can have the same representation as the parent type. Note that a private extension may always be converted to its parent type (it truncates). S.7.4.3. Operations of a Private Type [originally 7.4.2] S.7.4.4. Deferred Constants [originally 7.4.3] In Ada 83, deferred constants are only permitted in a visible part of a package if their type is private, and is declared in the same visible part (RM 7.4(4)). For Ada 9X, we are relaxing this restriction, so that a deferred constant of any type may be declared immediately in the visible part of a package, so long as the full constant declaration is given in the private part of the package. This eliminates the anomaly that prevented a constant of a composite type with a component of a private type from being declared, if the composite type was declared in the same visible part as the private type. Another advantage of deferred constants is that in some cases, the initial value depends on attributes of objects or types that are declared in the private part. For example, one might want to export a constant access value designating a read-only memory area: type ROM_TYPE is array(1..ROM_SIZE) of WORD; type ROM_PTR is access constant ROM_TYPE; ROM : constant ROM_PTR; -- deferred constant private ACTUAL_ROM : aliased ROM_TYPE; -- is a variable so don't need an initial value pragma INTERFACE_OBJECT(ACTUAL_ROM); ROM : constant ROM_PTR := ACTUAL_ROM'ACCESS; -- full definition of deferred constant S.7.4.5. Limited Types [originally 7.4.4] Functions that return limited types can compromise the general requirement that objects of a limited type cannot be copied. This is worse when the object whose value is being returned is a local object. For example, if a local object has any ``finalization'' associated with it (such as a task object, which is awaited as part of its finalization), then returning the value of such an object will generally not work properly, because the object's value will be copied into a temporary (presumably) prior to the finalization action, and then the finalization action may render meaningless the value stored in the temporary. One way to avoid this problem would be to provide a mechanism whereby a user-defined copy operation is executed on function return to initialize the temporary holding the return value, rather than a simple ``byte-copy'' of the object whose values is being returned. However, this would not solve the existing problem relating to returning a local task, since copying a task is not well-defined. Furthermore, the temporary object itself must eventually be finalized. Rather than requiring user-defined copy operations and the finalization of temporaries, we have proposed to simply defer all finalization actions of a function with a limited result type. After the result of the function has been ``used,'' the finalization actions of the function are performed. This approach nicely solves the local task problem, because it is not awaited and cleaned up until after the caller is through with it. Furthermore, it minimizes the total amount of finalization and copying that is required. The implementation of finalization deferral can be quite straightforward. There are two basic approaches. First, the function returns without cutting back its stack, and then when the caller is through with the result, it executes the finalization actions of the function and cuts back the stack. This is analogous to what is done with local package subunits, which can declare local tasks that are not awaited until the enclosing scope is finished. The second approach is to transform the function into a procedure that takes an extra parameter, which is a reference to a ``continuation'' procedure. The continuation procedure takes the function result as a parameter, uses it, and then returns. The body of the continuation procedure consists of the code that immediately followed the original function call and used its result. It is very important that the use of functional notation be available for all types, including types with finalization. Without the mechanism described here, or one like it, such functions would be impossible to write. It would be unfortunate if adding a finalization operation to an abstract data type required rewriting all uses of functions that return the type to use procedures and explicitly-declared temporary variables instead. S.7.5. Example of a Table Management Package S.7.6. Example of a Text Handling Package S.8. Ada 9X Visibility Rules S.8.1. Declarative Region S.8.2. Scope of Declarations The depth of scope levels is defined to represent the lifetimes of various run-time entities. The purpose is to prevent dangling pointers that might occur if a pointer lives longer than the entity it points at. The ``pointers'' involved here are normal Ada access values, as well as the internal pointers to type descriptors that an implementation will normally create for tagged types. S.8.3. Visibility In Ada 83, the basic operations of a type, such as ``:='' and ``or else,'' are directly usable anywhere objects of the type are used. However, operators such as ``='' and ``xor,'' and character literals such as `A', are not directly usable if the declaration of the type is not directly visible. Instead, selected component notation is necessary (such as P."="(X, Y)), or, in the case of an operator, a rename of the operator may be defined. Alternatively, the declaration of the type can be made directly visible via a use clause. From a typical programmer perspective, the distinction between a basic operation like or else and the predefined operator or may seem somewhat artificial. Therefore, for Ada 9X, we are extending the universal usability of basic operations to include primitive operators and character literals. A primitive operator of a type is one declared immediately in the same visible part as the type, whether the declaration is implicit or explicit. The net effect is that all primitive operations of a type denoted with a non-identifier syntax are usable without the need for selected component notation. Such operations are considered primitively visible. S.8.4. Use Clauses Child library units are described in MS-10.1. As explained there, they are treated like separately compiled but logically nested units, which are visible only when mentioned in a with clause. To ensure that they may be used like nested units, the simple name of a child mentioned in a with clause becomes directly visible when the logically enclosing parent package is mentioned in a use clause. For example, using the example from MS-10.1: with OS.FILE_MANAGER; procedure HELLO is use OS; -- This use clause makes the name FILE_MANAGER directly visible -- as well as other declarations in package OS. FILE : FILE_DESCRIPTOR := FILE_MANAGER.OPEN("hello.txt", FILE_MANAGER.WRITE_ONLY); begin FILE_MANAGER.WRITE(FILE, "Hello World."); FILE_MANAGER.CLOSE(FILE); end HELLO; S.8.5. Renaming Declarations To enhance the usefulness of renaming, we are proposing that the body of a subprogram may be provided with a renaming declaration. The renaming may appear anywhere after the subprogram declaration, so long as it is immediately within the same declarative region. If the subprogram declaration is in a package specification while the subprogram definition via a renaming is in a package body, the renaming must be of a subprogram that has subtype conformance (MS-6.3.1(5)) with the subprogram's declaration. This ensures that the caller of the subprogram will perform the correct constraint checks on the actual parameters, and pass the parameters following the correct calling convention (MS-13.9(3)), seeing only the subprogram's specification. A normal subprogram renaming requires only mode conformance (MS-6.3.1(4)). This kind of conformance is too weak for a renaming provided in the body. Given only mode conformance, the caller might perform constraint checks that were too stringent or too lax, and might pass parameters following the wrong calling conventions, putting them in the wrong place on the stack, or in the wrong register. We considered requiring subtype conformance for all subprogram renaming. However, this introduces upward incompatibilities, particularly given the existing equivalence between generic formal subprogram matching and renaming (see RM 12.3.6(5)). Furthermore, it is not always possible to statically match the subtype of a formal parameter of a subprogram, if the subprogram is implicitly declared as part of the type definition. In particular, if the subprogram is derived from a parent type, then the formal parameter subtypes have the constraints that were present on the parent type's subprogram (see RM 3.4(13)). If the derived type definition itself imposes a constraint, then it is likely that the constraint on the formal parameter of the derived subprogram is actually looser than the constraint on the first named subtype of the derived type (see RM 3.4(3)). This means there is no namable subtype that has constraints as loose as those on the formal parameter. S.8.6. The Package STANDARD S.8.7. The Context of Overload Resolution S.9. Ada 9X Tasks and Synchronization S.9.1. Task Specifications and Task Bodies For Ada 9X, we have generalized discriminants so that they are applicable to all composite types. In particular, task types may have discriminants, allowing task objects to be parameterized at creation time. This obviates the need for an initial rendezvous to inform the task of what objects it is to control or manipulate. By so doing, it eliminates a serial bottleneck that can interfere with the parallel activation of tasks. S.9.2. Task Types and Task Objects Task types are limited. In Ada 83, limited types could not be used for out parameters (except for a subprogram whose body is within the scope of the full non-limited type declaration -- RM 7.4.4(4)). For Ada 9X, we have removed the restrictions associated with out parameters (MS-6.2(4)) while also ensuring that objects do not become ``de-initialized'' as a result of being passed as an out parameter (MS-6.2(7)). This eliminates the need for any restrictions on the use of limited types and out parameters. We have proposed a class-wide type for the task class, root_task'CLASS. The class-wide type simplifies the definition of operations applicable to all tasks, such as dynamic priority control operations, task attribute definition, etc. See the Task Identification section of MS-G, Systems Programming Annex and the Dynamic Priorities section of MS-H, Real-Time Systems Annex for examples of use of the class-wide task type. We proposed the name TASK_CLASS for this type, but to be consistent with the shift in names planned for the root numeric class-wide types, we now plan to name the class-wide task type ROOT_TASK_CLASS. S.9.3. Task Execution - Task Activation S.9.4. Task Dependence - Termination of Tasks S.9.5. Protected Record Specification and Protected Bodies [new] In Ada 83, tasks are used both for representing logical parallelism in a program, and for synchronizing that parallelism when necessary to access shared data structures. The rendezvous is used both for communication and synchronization. However, the very generality of the rendezvous has made it a relatively high overhead mechanism. For Ada 9X, we are introducing a low overhead, data-oriented synchronization mechanism called protected records. From the client perspective, operating on a protected record is similar to operating on a task object. The operations on a protected record allow two tasks to synchronize their manipulations of shared data structures. From the implementation perspective, a protected record is designed to be a very efficient conditional critical region. The protected operations are automatically synchronized to allow only one writer, or multiple readers. The protected operations are defined using a syntax like a normal subprogram body, with the mutual exclusion of the critical region happening automatically on entry, and being released automatically on exit. We considered many different approaches to satisfying the needs for fast mutual exclusion, asynchronous communication, and various other common real-time paradigms. We settled on the protected record construct because it seemed to provide a very efficient building block, which was flexible enough to implement essentially any higher-level synchronization mechanism of interest. Some of the features that make protected records attractive as a building block are: Scalability Protected records enable the implementation of a synchronization mechanism that scales smoothly from a single processor to a multiprocessor. There is no built-in bias to a monoprocessor nor to a multiprocessor. Adaptability Additional protected operations may be added to a protected record without the need to modify the existing operations. Approaches that use explicit signals and conditions rather than the conditional barrier approach generally need each operation to be aware of every state of interest, and explicitly signal all possible waiting tasks. Modularity All of the operations of a given critical region are identified in the specification of the protected record, and their implementations are encapsulated within the body of the protected record unit. All of the protected data components are encapsulated within the specification of the protected record, and all of the interesting states are identified explicitly via entry barrier conditions within the body of the protected record unit. Efficiency The size and initialization requirements of a protected record are known to the client, because all entries and data components are declared in the specification. This allows protected records to be allocated statically or directly on the stack, rather than via dynamic allocation, and to be initialized in-line. No extra context switches are required to service waiting clients, since the task changing the state may directly execute the entry bodies whose barriers become true. Non-queued locking may be used to implement the mutual exclusion of a protected record because no suspension is permitted during the execution of a protected operation. Expressiveness The specification of a protected record makes explicit distinctions between read-only operations (functions), read- write operations (procedures), and possibly suspending operations (entries). This distinction is vital in analyzing a real-time program for correctness, including freedom from deadlock. Compatibility The entry call remains the primary mechanism for suspending a task until some condition is satisfied. An entry call on another task is suspended until the corresponding entry in the task is open. An entry call on a protected record is suspended until the corresponding entry barrier is true. An entry call on a task is completed when the rendezvous finishes. An entry call on a protected record is completed when an entry body finishes, possibly with intermediate suspensions as part of a requeue. From the caller's perspective, an entry call involves a possible initial suspension, one or more intermediate suspensions, and then a return. During the entry call, data may be transferred via the parameters. By using the entry call interface for both protected records and tasks, the existing conditional and timed entry call mechanisms are applicable, as well as the generalized selective entry call (see MS-9.9.2). Interrupt Handling A protected record procedure is very well suited to act as an interrupt handler. The nonsuspending critical region matches the needs of an interrupt handler, as well as the needs of non-interrupt-level code to synchronize with an interrupt handler. The entry barrier construct allows an interrupt handler to signal a normal task by changing the state of a component of the protected record and thereby making a barrier true. Of the many other approaches we considered for supporting data-oriented synchronization, none could match this set of desirable features. S.9.6. Protected Record Types and Protected Record Objects [new] Like task types, protected record types are limited types. Because protected records are specifically designed for synchronizing access from concurrent tasks, a formal parameter must always denote the same protected record as the corresponding actual parameter (a copy will not not preserve the required atomicity). A protected record type may have discriminants, to minimize the need for an explicit initialization operation, and to control variants or composite components of the protected record. The other data components of a protected record must be declared in the specification of the type to ensure that the size is known to the compiler when the type is used by a caller, but these components are only accessible from protected operations defined in the body of the protected record unit. S.9.7. Protected Operations, Entry Calls, and Accept Statements [originally 9.5] Protected records may have protected operations that are functions, procedures, or entries. All the entries must be declared in the protected record specification to ensure that space needed for entry queues is included when allocating the protected record. Additional functions and procedures may be declared in the body of the protected unit, for modularizing the implementation of the operations declared in the specification. The Mapping Specification includes an example of a counting semaphore implemented as a protected record type (see MS-9.6(7-11)). This example illustrates the three kinds of protected operations: functions, procedures, and entries. Functions provide read-only access (which may be shared) to the components of the protected record. Procedures provide exclusive read-write access to the components. Entries have an entry barrier that determines when the operation may be performed. The body part, if present, is performed with exclusive read-write access to the components, once the barrier becomes true due to the execution of some other protected operation. The evaluation of barrier expressions is also performed with exclusive access to the protected record. Here is an example of use of the counting semaphore: MAX_USERS : constant := 10; -- Limit number of simultaneous users of service USER_SEMAPHORE : COUNTING_SEMAPHORE(MAX_USERS); procedure USE_SERVICE(P : PARAM) is begin -- Wait if too many simultaneous users USER_SEMAPHORE.ACQUIRE; begin PERFORM_SERVICE(P); exception when others => -- Always release the semaphore for next user. USER_SEMAPHORE.RELEASE; raise; end; -- Release the semaphore for others USER_SEMAPHORE.RELEASE; end USE_SERVICE; Some of the protected operations declared in the specification may be declared after the reserved word private. This makes these operations callable only from within the protected unit. We have proposed a similar feature for task types, so that certain task entries may be hidden from a direct call from outside the task (they can be called by its sub-tasks). We originally proposed that the private part of a protected unit would be visible to the enclosing package. However, this was considered confusing by some reviewers, and felt to be inconsistent with the visibility of the private part of a subpackage. We also considered splitting the protected record (and task) specification into two separate parts, with the private operations and data components declared in a second part included inside the private part of the enclosing package. However, this seemed like an unnecessary extra syntactic complexity, so we instead adopted the simpler suggestion from two Revision Requests (RR-0487, RR-0628) of using private within the specification to demarcate the private operations. Each entry declared in the specification of a protected record must have an entry body. An entry body consists of an entry specification, a barrier condition following the reserved word when, and an optional body part to specify an action to be performed once the barrier becomes true. The entry body for an entry family must specify a name for the entry family index, using an iterator notation (for I in discrete_range). We considered the simpler syntax ``I : discrete_range,'' however we opted for the iterator notation to avoid ambiguity with the formal parameter part, and to be consistent with other constructs for naming an index covering a discrete range (MS-4.3.2(4), MS-9.9(3)). An entry barrier is not allowed to depend on parameters of the entry, but it may depend on the entry family index, or any other data visible to the entry body. This rule ensures that all callers of the same (single) entry see the same barrier condition, allowing the barrier to be checked without examining individual callers. Without this rule, each caller of a given entry would have to be treated separately, since each might have a different effective barrier value. Rather than entry ``queues'' one would essentially have a single large ``bag'' of callers, all of which would have to be checked on each protected record state change. For flexibility, entry barriers may depend on data global to the protected record. This allows part of the data managed by the protected record to be outside of the record, when this is necessary due to some other program structure requirements. However, the barriers are only checked after completing a protected procedure or entry body, so asynchronous changes to global data have no immediate effect on the eligibility of a caller waiting on an entry queue. For efficiency, implementations may assume that the only meaningful changes to data referenced in an entry barrier of some protected record take place within a protected operation of that protected record. We considered disallowing references to globals in a barrier expression. However, that would also disallow the use of functions that reference globals or the dereferencing of access values. It was felt to be more complex to implement, and too restrictive when dealing with data types implemented with access types, such as a linked list. The semantics for protected records are described in terms of mutual exclusion (except that protected functions may execute concurrently). In addition, as the final step of a protected action, the entry queues are serviced before allowing new calls from the outside to be executed. In this context, a protected ``action'' is either a call on a protected subprogram from outside the protected unit, the execution of an entry body, or the addition or removal of a call from an entry queue. Servicing the entry queues is required if any change has been made that might affect a barrier expression value. First the barriers for the non-empty entry queues must be reevaluated. If at least one such barrier evaluates to true, some eligible caller must be selected, and the corresponding entry body must be executed. The barriers are then reevaluated again, and this process continues until all non-empty entry queues have a false barrier. The barriers may be evaluated, and the entry bodies executed, by any convenient thread of control. It need not be the thread of the original caller. This flexibility allows for the most efficient implementation, minimizing unnecessary context switches. While executing a protected operation of some protected record, a task may not call a potentially suspending operation for any reason, though it may release the mutual exclusive access to the protected record by being requeued. Disallowing suspension while executing a protected record operation allows a nonqueued locking mechanism to be used to implement the mutual exclusion. If suspension were allowed, then a queued locking mechanism would be required, since potential callers might attempt to get the lock while the current holder of the lock is suspended. In the simplest monoprocessor environment, protected record mutual exclusion may be implemented by simply inhibiting all task preemption. If multiple priorities are supported, then rather than inhibiting all preemption, a ceiling priority may be established for the protected record (see the Ceiling Priorities section of MS-H, Real-Time Systems Annex). Only tasks at this ceiling priority or below may use the protected record, meaning that tasks at priorities higher than the ceiling may be allowed to preempt the task performing the protected operation while still avoiding the need for a queued lock. In a multiprocessor environment, spin waiting may be used in conjunction with the ceiling priority mechanism to implement a non-queued protected record lock. By disallowing suspension within a protected operation and by also using the ceiling priority mechanism, unbounded priority inversion can be avoided. The generality that might be gained by allowing suspension, would inevitably result in an increase in implementation complexity, run-time overhead, and unbounded priority inversion. To simplify composability, protected operations may call other non-suspending protected operations (i.e. protected procedures and functions). A direct call on a protected subprogram within the same protected record does not start a new protected action, but is rather considered to be part of the current action. It is also considered an error if, through a chain of calls going outside the protected record, a call is made back to the same protected record object. The effect is implementation-defined, but will generally result in a deadlock. We considered disallowing all subprogram calls from a protected operation to a subprogram defined outside the protected record, but this seemed unnecessarily constraining, and severely limited composability. As with task entries, protected record entries have a COUNT attribute. This attribute may be referenced within barrier expressions, to intentionally create a service order among the entries of a protected record. The COUNT attribute can also be used to create transient conditions, which only persist as long as an entry queue is non-empty, and then are reset. Example of the use of COUNT to create a transient condition: protected type TRANSIENT_SIGNAL is entry WAIT; -- A caller is suspended until a SIGNAL is received. entry SIGNAL; -- All waiting callers are resumed, -- and then the signal is reset. private record null; -- No data components needed, -- COUNT attributes are sufficient. end TRANSIENT_SIGNAL; protected body TRANSIENT_SIGNAL is entry WAIT when SIGNAL'COUNT > 0; -- A caller is suspended until a SIGNAL is received. entry SIGNAL when WAIT'COUNT = 0; -- A caller of SIGNAL is queued until all -- waiting callers have been resumed, -- and then the entry call returns. end TRANSIENT_SIGNAL; When the WAIT entry is called, the caller will be suspended until some caller is enqueued on the SIGNAL entry queue. When the SIGNAL entry is called, the caller is queued if there are any WAITers, then all the tasks on the WAIT entry queue are resumed. The SIGNALer is then dequeued and the entry call is complete (since it has no body part). If there are no WAITers when SIGNAL is called, it returns immediately. If COUNT attributes are not referenced, then the barriers are only dependent on the (non-transient) state of the data components, thereby minimizing the possibility of undesirable race conditions. The COUNT attributes are available for exactly those situations where race conditions are intentional (as in the transient signal example). The ease of implementing race-free synchronization mechanisms, while still being able to create, when desired, a synchronization mechanism based on transients, illustrates the combined flexibility and safety of protected records. S.9.7.1. Requeue Statements [new] The requeue statement is designed to handle the following situations: - After an accept statement or entry body begins execution, it may be determined that the request cannot be handled immediately. Instead, there is a need to requeue the caller again, until the request can be handled. - Alternatively, part of the request may be handled immediately, but there may be additional steps in the process that need to be performed at a later point. In both cases, the accept statement or entry body needs to ``complete'' so other callers may be handled, or other processing may be performed. The requeue statement allows a caller to be ``requeued'' on the queue of the same or some other entry. The caller need not be aware of the requeue. The number of steps required to handle a given operation need not be visible outside the task or protected record. The net effect is that the server can be more flexible, while retaining the simple entry-call interface for the client. As part of the requeue, the parameters are neither respecified nor reevaluated. Instead, the formal parameters are carried over to the new call directly. If a new parameter list were specifiable, then it might include references to data local to the accept statement or entry body itself. This would not work because the accept statement or entry body is being completed, and all of its local variables are being deallocated. Subtype conformance is required between the new target entry (if it has any parameters) and the current entry. This allows the same representation to be used for the list of formal parameters, be they by-copy or by-reference. This also eliminates the need to allocate new space to hold the formal parameters. As a further restriction, a requeue from a protected record must be to another protected record entry. It was felt to be an undue implementation burden to allow a requeue from a protected record entry to a task entry. Requeue, when combined with a simple ``signal'' protected record (see the Processor Time Accounting section of MS-H, Real-Time Systems Annex), provides the basic mechanism for suspending a caller from inside a task or protected record. For example, here is a printer manager that passes print requests to one of several printers. It uses requeue to suspend the caller until its printing job is complete: package PRINTER_PKG is task PRINTER_SERVER is entry PRINT(FILE_NAME : STRING); -- Print file on some printer. -- Entry call accepted when a printer is available. -- Entry call returns when printing is complete. end PRINTER_SERVER; end PRINTER_PKG; package body PRINTER_PKG is type PRINTER_INFO is record . . .; protected type PRINTER is -- Device driver for a printer. procedure START(FILE_NAME : STRING); -- Initiate printing entry DONE; -- Barrier true when printing complete procedure INITIALIZE(INFO : PRINTER_INFO); -- Initialize device, given info on device. private procedure HANDLE_INTERRUPT; -- Interrupt handling procedure. record . . . -- implementation dependent end PRINTER; type PRINTER_ID is range 1..NUM_PRINTERS; PRINTER_ARRAY : array (PRINTER_ID) of PRINTER; PRINTERS_INFO : constant array (PRINTER_ID) of PRINTER_INFO := (1 => info on printer # 1, 2 => . . . ); task body PRINTER_SERVER is PRT : PRINTER_ID; -- An available printer begin -- Initialize printer array. for I in PRINTER_ID loop PRINTER_ARRAY(I).INITIALIZE(PRINTERS_INFO(I)); end loop; loop -- Wait for some printer to be available. select for I in PRINTER_ARRAY'RANGE PRINTER_ARRAY(I).DONE; -- Barrier true when not busy PRT := I; -- Remember available printer end select; select -- Wait for caller or terminate. accept PRINT(FILE_NAME : STRING) do -- Start printing the file and requeue caller. PRINTER_ARRAY(PRT).START(FILE_NAME); requeue PRINTER_ARRAY(PRT).DONE with abort; end PRINT; or terminate; end select; end loop; end PRINTER_SERVER; end PRINTER_PKG; This example illustrates how requeue can be used to suspend the caller and go on and handle another caller or perform other activities in the task. This example also illustrates the selective entry call with an iterator (see MS-9.9(16)). In the above example, the requeue statement includes the reserved words with abort. This specifies that abort is allowed while waiting for the next entry call to begin. In this example, it makes sense because there is no reason why the caller must be retained on the DONE queue once the printing has commenced. The printing can continue even though the caller was aborted or underwent an asynchronous transfer of control (see MS-9.9.3). In other situations, the default requeue that defers abort will be preferred, because the multi-step operation is presumably in some critical stage that cannot be abandoned without corrupting shared data structures. This behavior is specified as the default in order to be consistent with the normal deferral of abort once a rendezvous has started. S.9.8. Delay Statements, Duration, and Time [originally 9.6] Ada 83 provides only one clock that may be used directly for delaying a task or timing an operation. Furthermore, the Ada 83 delay statement requires a duration, rather than a wakeup time, making it difficult for a task to wake up at perfectly regular intervals. For Ada 9X, we augment the delay statement with a delay until statement. The delay-until statement takes a wakeup time, rather than a duration. Furthermore, in the Monotonic Time section of MS-H, Real-Time Systems Annex we define an additional time type MONOTONIC.TIME with an accompanying function MONOTONIC.CLOCK, which may be used in a delay until statement. The new time type MONOTONIC.TIME is meant to represent a real-time clock with potentially finer granularity than the time-of-day clock associated with CALENDAR.TIME. Furthermore, the value returned by MONOTONIC.CLOCK is guaranteed to be monotonically non-decreasing, whereas the time-of-day CALENDAR.CLOCK may jump forward or backward due to resetting of the time by a human operator. S.9.9. Select Statements [originally 9.7] Ada 83 allowed multiple accept alternatives in the selective wait statement, but did not allow multiple entry call alternatives in a select statement. For Ada 9X, we are making the select statements more symmetric, by allowing multiple entry call alternatives in a selective entry call statement. (We have also renamed the selective ``wait'' statement to be the selective accept statement in order to emphasize the symmetry and avoid confusion.) Allowing multiple entry call alternatives is crucial to supporting asynchronous communication with multiple correspondents. For example, a message routing task may be simultaneously trying to write into some mailboxes, while waiting to read from others. If the outgoing mailboxes are full, or the incoming mailboxes are empty, it it important to be able to wait on multiple mailboxes to avoid the need for polling. Assuming each mailbox is represented by a protected record, waiting on multiple mailboxes translates into performing multiple entry calls in parallel, waking up only after one (and only one) of them has been accepted. In addition to allowing multiple entry call alternatives, we have also proposed iterators to better support entry families and arrays of tasks or protected records. In Ada 83, there is no mechanism for opening up all entries in a family, without enumerating them explicitly in the selective accept statement. If the number of entries in the family is not known at compile time, then it is essentially infeasible to open all of them at once. For Ada 9X, an iterator notation ``for identifier in discrete_range'' may be included in the select alternative, and the identifier may be referenced in the guard or elsewhere in the select alternative. This is considered equivalent to a sequence of select alternatives with any references to the identifier of the iterator substituted with the corresponding value from the discrete range. This allows direct support for entry families, or for entry calls on an array of tasks or protected records. See the examples in MS-9.9 and MS-9.9.1. We considered allowing intermixing of accept alternatives and entry call alternatives within the same select statement. However, there were significant semantic and implementation issues: - When priority scheduling is in effect (see the Priority Queuing section of MS-H, Real-Time Systems Annex), accept alternatives are selected on the basis of the priority of the caller. By contrast, entry call alternatives are selected (if more than one barrier is open) based on the order of appearance in the select statement (see MS-9.9.2(3)). - If a select statement included both entry call and accept alternatives, then when an entry call was accepted, it would not only need to atomically close all other entries of the same accepting task, but it would need to close all entries of the calling task, and cancel all pending entry calls of both the calling and accepting task before they were accepted by other tasks. Performing all of these actions atomically was felt to represent a serious implementation difficulty in a multiprocessor environment. The selective entry call with multiple entry call alternatives does not impose a similar implementation burden, as the necessary coordination can be performed using a single test-and-set flag associated with the select statement. In Ada 83, because only one entry call alternative was permitted, and all communication was synchronous, a single task was never queued on more than one entry queue (and possibly on the delay queue) at the same time. In Ada 9X, a single task may appear on multiple entry queues simultaneously. This is inevitable in the context of full asynchronous communication. One of the important reasons for asynchronous communication primitives is that a single task may have pending communications with multiple correspondents. Furthermore, support for asynchronous transfer of control (see MS-9.9.3) implies that a task needs to simultaneously await some interrupting event, while executing other operations, including possibly communication operations. Support for waiting on multiple entry queues can be provided by allocating at the call site one ``queue element'' (or ``agent'' TCB) for each entry call (or delay) alternative, to be placed on the associated entry (or delay) queue. The queue element would identify the select statement and the calling task, allowing appropriate coordination and synchronization among the multiple pending calls. Each queue element would also point to the parameters being passed to the entry. Conceptually, the queue element acts as a very lightweight agent for the calling task, allowing the task to go on to perform other operations or be enqueued on other entry queues. S.9.9.1. Selective Accept [originally 9.7.1 ``Selective Wait''] The only syntax change proposed for the selective accept statement (other than its new name) is the iterator construct to better support entry families (see example in MS-9.9.1). In the Priority Queuing section of MS-H, Real-Time Systems Annex, priority-based selection among multiple open accept alternatives is provided as a program-wide option selected by a pragma. S.9.9.2. Selective Entry Calls [consolidation of 9.7.2 and 9.7.3] As discussed above in S.9.9, the conditional and timed entry call statements of Ada 83 are being unified as a single selective entry call statement. There may be multiple entry call alternatives and multiple delay alternatives, with an optional final else part. Instead of the else part, an abortable final part may be specified by using the reserved words then abort. This is discussed below in S.9.9.3. S.9.9.3. Asynchronous Transfer of Control [new] Asynchronous transfer of control was identified as an important requirement for Ada 9X (Requirement R5.3-A(1)). In Ada 83, the only way to asynchronously change the execution path of a task is to abort it. However, in many applications, it is desirable that an external event be able to cause a task to begin executing at a new point, without the task as a whole being aborted and restarted. As an example of asynchronous transfer of control, consider an interactive program where the user may choose to terminate a given operation and begin a new one. This is normally signaled by typing a special key or hitting a special button associated with the controlling input device. The user does not want the entire context of the running program to be lost. Furthermore, for a long-running system, it is important that the resources associated with the interrupted processing be reclaimed. This implies that some mechanism for ``cleaning up'' as part of the asynchronous transfer be available. Finally, if the abortable operation is updating some global data structure, it is essential to temporarily defer any asynchronous transfers, until the update is complete. In Ada 9X, a selective entry call with an abortable final part is used to support asynchronous transfer of control. Like a final else part, an abortable final part runs if none of the select alternatives is immediately selected. However, in contrast to an else part, the select alternatives are not cancelled when the abortable final part begins, but instead remain pending. If one of them is selected before the final part completes, then the sequence of statements of the abortable final part is aborted, and an asynchronous transfer of control takes place to the sequence of statements of the selected alternative. By supporting asynchronous transfer of control as part of a selective entry call, several useful properties are provided: - The statements that are abortable are clearly bracketed in the abortable final part. - The asynchronous transfer of control is directly tied to the acceptance of an entry call or the expiration of a delay. Only one such event at a time can affect the target task. This allows the transfer to occur without requiring another task to explicitly signal each task (see examples in MS-9.9.3), in contrast to what is possible with abort. - Any one of a set of events, represented by the select alternatives, can be used to trigger the asynchronous transfer. Which event occurred, if any, can be determined, because the sequence of statements of the corresponding alternative is executed. - Nesting of abortable regions (which are potentially ``sensitive'' to different events) and protecting code in these regions from interruption, is naturally achieved by the select construct and protected records. - The asynchronous transfer cannot be mistakenly redirected by a local handler, as might happen with a mechanism based on asynchronous exceptions. Here is an example of a database transaction using asynchronous transfer of control (see MS-9.9.3 for other examples). The database operation may be canceled by typing a special key on the input device. However, once the transaction begins to commit, the operation may not be cancelled: package TXN_PKG is type TXN_STATUS is (INCOMPLETE, FAILED, SUCCEEDED); type TRANSACTION is new CONTROLLED with private; -- Transaction is a controlled type, see MS-G, Systems Programmin procedure FINALIZE(TXN : in out TRANSACTION); procedure SET_STATUS(TXN : in out TRANSACTION; STATUS : TXN_STATUS); private type TRANSACTION is new CONTROLLED with record STATUS : TXN_STATUS := INCOMPLETE; pragma ATOMIC (STATUS); . . . -- More components end record; end TXN_PKG; package body TXN_PKG is procedure FINALIZE(TXN : in out TRANSACTION) is begin -- Finalization runs with abort and ATC deferred if TXN.STATUS = SUCCEEDED then COMMIT(TXN); else ROLLBACK(TXN); end if; end FINALIZE; procedure SET_STATUS(TXN : in out TRANSACTION); STATUS : TXN_STATUS) is begin TXN.STATUS := STATUS; end SET_STATUS; end TXN_PKG; declare DATABASE_TXN : TRANSACTION; -- Declare a transaction, will commit or abort -- during finalization begin select -- Wait for a cancel key from the input device INPUT_DEVICE.WAIT_FOR_CANCEL; -- The STATUS remains INCOMPLETE, so that -- the transaction will not commit then abort -- Do the transaction READ(DATABASE_TXN, ...); WRITE(DATABASE_TXN, ...); . . . SET_STATUS(DATABASE_TXN, SUCCEEDED); -- Set status to ensure the transaction is committed exception when others => PUT_LINE ("Operation failed with unhandled exception"); -- Set status to cause transaction to be aborted SET_STATUS(DATABASE_TXN, FAILED); end select; -- FINALIZE on DATABASE_TXN will be called here and, -- based on the recorded status, will either commit or -- abort the transaction. end; S.9.10. Priorities [originally 9.8] The discussion of priorities has been moved to the Priority Queuing section of MS-H, Real-Time Systems Annex. Without specifying some minimum range of priorities, there seemed to be no point in specifying a priority model at all within the ``core'' of the standard. Furthermore, by moving the discussion of priorities to an annex section, Ada 9X can more easily accommodate multiple scheduling models rather than only a strict priority-based one. S.9.11. Task, Protected Record, and Entry Attributes [originally 9.9] S.9.12. Abort of a Task; Abort of a Sequence of Statements [originally 9.10] For Ada 9X, it is essential that use of the abort statement, and, more importantly, the select statement with an abortable final part (MS-9.9.3), not result in corruption of global data structures. In Ada 83, abort was deferred for a calling task while it was engaged in a rendezvous. This allowed the rendezvous to complete normally so that the data structures managed by the accepting task were not left in an indeterminate state just because one of its callers was aborted. For Ada 9X, we have generalized this deferral of abort to include the duration in which calls on protected operations are being serviced, and the initialization and finalization of controlled types (see MS-G, Systems Programming Annex). (Note, that requeue with abort allows a server to override this deferral.) Without deferral of abort, any update of a global data structure becomes extremely unsafe, if not impossible. Ultimately all updates are forced into a two-phase approach, where updates are first performed into unused storage, and then the final commitment of a change involves a single atomic store, typically of a pointer of some sort. Such an approach can be extremely cumbersome, and very inefficient for large data structures. In most cases, it is much simpler and efficient to selectively defer asynchronous transfers or aborts, rather than to allow them at any moment. In addition to deferring abort, it is important to be able to reclaim resources allocated to frames being aborted. The support for user-defined initialization and finalization of controlled types provides the primitives necessary to perform appropriate resource reclamation, even in the presence of abort and asynchronous transfers. Reclaiming local storage resources is of course important. However, releasing resources is even more critical for a program involved in communicating with an external subsystem, such as a remote database or other server. For a short-lived program, running on a conventional time-shared operating system, with no contact with external subsystems, it might be argued that there is no need to provide user-defined finalization that runs even when the task is aborted or is ``directed'' to execute an asynchronous transfer of control. However, for a long-running program, with limited memory, and which is possibly communicating with external subsystems, it is crucial that relatively local events like an asynchronous transfer not undermine global resource management. S.9.13. Shared variables [originally 9.11] The discussion of shared variables has been moved to the Shared Variable Control section of MS-G, Systems Programming Annex, where the capabilities are extended to support the functionality described in Requirement R7.1-A(1) -- Control of Shared Memory. Since memory architectures are so diverse, it was deemed more appropriate to include such details in an annex section dedicated to a domain where such low-level dependencies are more common. S.9.14. Example of Tasking and Synchronization [originally 9.12] S.10. Ada 9X Program Structure and Compilation Issues S.10.1. Compilation Units - Library Units In Ada 83, if a package does not require a body, but has one nevertheless, its body can become out-of-date, and be silently omitted from a subsequent build of an executable program. This can lead to mysterious run-time failures due to the lack of the package body. By requiring every library unit package to have an explicit body, even if null, this problem is avoided. An out-of-date body for a package referenced in a program will always result in an error at program build time. In Ada 83, there are a number of situations where relatively small changes result in large numbers of recompilations. For example, there is no way to define an additional operation for a private type without recompiling the package where the type is declared. This forces all clients of this package to become out-of-date, even though they don't need the new operation. Massive recompilations can result from what is fundamentally a very small change. For Ada 9X, we are addressing the recompilation problem by allowing a library unit package to be effectively extended with child library units. A child library unit is an independent library unit in that it is not visible unless referenced in a with clause. However, a child library unit may be used to define new operations on types defined in the parent package, because the private part and body of the child unit have visibility onto the private declarations of the parent package. The name of a child library unit indicates its position in the hierarchy. Its name is an expanded name, with the prefix identifying its parent package. Furthermore, when a child library unit is referenced in a with clause, it ``looks like'' a unit nested in its parent package. This allows the existing naming and visibility rules of nested units to be carried over directly when using child library units. If a child library unit is not mentioned in a with clause, it is as if it didn't exist at all. Adding a new child library unit never causes any immediate recompilation of existing compilation units. Of course, eventually some number of other library units will come to depend on this child library unit, and then recompiling the child will affect these ``client'' units. But by distributing the set of operations across multiple children, the number of clients affected by any single change can be kept to a minimum. Furthermore, the with clause provides explicit and detailed indications of interdependencies, helping to document the overall structure of the system. Child library units provide a combination of compilation independence and hierarchical structuring. This combination results in an extremely flexible building block for constructing subsystems of library units that collectively implement complex abstractions. See MS-10.1 for an example of a hierarchy of library units implementing a subsystem representing an operating system interface. S.10.1.1. Context Clauses - With Clauses As in Ada 83, a with clause is used to make other library units visible within a compilation unit. For Ada 9X, to support the identification of a child library unit, the with clause syntax is augmented to allow the use of an expanded name consisting of a sequence of identifiers separated by periods, rather than just a single identifier. Once the child library unit has been identified with its full name in the with clause, normal renaming or use clauses may be used within the compilation unit to shorten the name needed to refer to the child. In addition to renaming from within a unit, renaming may be used at the library unit level to provide a shorter name for a child library unit, or to hide the hierarchical position of a child unit when appropriate. For example, a library unit may be defined as a child unit to gain special visibility on a parent unit, but this visibility may be irrelevant to most users of the child unit. Other reasons for library unit renaming: - essentially all hierarchical naming systems provide some kind of ``alias'' capability, because of the advantages of allowing the same entity to appear in multiple places within the hierarchy; - ``helper'' packages that use unchecked conversion to overcome the restrictions on private types may already exist; through the use of library unit renaming, such helper packages can remain available by their current root library unit names, while moving their actual definition into the appropriate place in the library unit hierarchy reflecting their access patterns; - it is a common practice to establish standard package renaming conventions throughout a project; without library-level renaming, each unit that "with"s a package must include a copy of the appropriate renaming declaration for the package; this introdues the possibility of typos and nonconformities, and, if done in the spec, overrides the useful lack of transitivity provided for names introduced by "with" clauses; the program library manager keeps a central record of the project-wide renaming declarations; - when integrating independently developed libraries into a single library, the hierarchical name space may be used to avoid collisions, and renaming may be used to selectively ``export'' units from their local name spaces into the root name space for a given user's library; - given a large program library, it may be useful to have a tailorable subsystem that includes "with"s for a certain set of units, and then use renaming to select the particular implementations of those units to be used in the particular subsystem configuration appropriate at a given time. Some of the capabilities provided by library unit renaming are currently supported by some program library managers. However, by standardizing these renaming capabilities, very large systems can be built and maintained and rehosted without becoming overly dependent on non-standard program library features. Library units may be identified as either private or visible. A private library unit may only be referenced in a with clause from a compilation unit within the hierarchy of library units rooted at the library unit's parent package. Furthermore, the declaration of a visible library unit may not depend on a private library unit with the same parent package. Both child library units and root library units may be marked as private. For root units, private has little direct effect. However we anticipate that some implementations may provide means for one program library to be referenced from another program library, and in this case the private marking might be used to limit visibility across program libraries. The ability to mark a library unit private is extremely valuable in establishing the same separation between interface and implementation at the subsystem level as is provided in Ada at the individual unit level. The private library units of a hierarchy, plus the bodies of both the private and visible library units of the hierarchy, make up the ``implementation'' of a subsystem. The ``interface'' to the subsystem is the declaration of the root package in the hierarchy, plus all of the visible descendant child library units. The rules governing interdependence of child library units guarantee that there are no ``backward'' dependences from some declaration in the interface of a subsystem onto some declaration in the implementation of a subsystem. For example, these rules prevent the implementation details of a private type from ``sneaking out'' to users of the subsystem. S.10.1.2. Main Subprograms, Partitions, and Environment Tasks [new] In Ada 83, an executable program consists of a main subprogram and all other library units reachable from this main subprogram. Execution proceeds by elaborating the entire program, running the main subprogram to completion, waiting for all library-level tasks to complete, and then terminating. Although this model for a program is appropriate in some environments, for many programming environments, a much more dynamic and distributed model is preferable. For Ada 9X, we have proposed that a program may be formed from a cooperating set of partitions. In MS-10.1.2, all that is said about these partitions is that they elaborate independently, communicate, and then terminate in some implementation-defined manner. Each partition has its own environment task to act as the thread of control for library-level elaboration, and to act as the master for library-level tasks. The description in MS-10.1.2 is kept purposefully non-specific to allow for many different approaches to dynamic and distributed program construction. However, in MS-I, Distributed Systems Annex, additional standard pragmas and attributes are defined to form the basis for a standard, portable approach to distribution. S.10.1.3. Examples of Compilation Units [originally 10.1.2] S.10.2. Subunits of Compilation Units Child library units take over some of the applications of subunits. However, subunits remain the only way for separately compiling a unit that has visibility to the declarative part of the enclosing unit's body. They are also appropriate for providing bodies for individual units that may be undergoing more active development or maintenance than surrounding units. To simplify the use of subunits, for Ada 9X we are eliminating the requirement on uniqueness of their simple name within an enclosing library unit (RM-10.2(5)). Only the expanded name need be unique. S.10.2.1. Examples of Subunits S.10.3. Order of Compilation S.10.4. The Program Library We anticipate that implementations will continue to support library unit references between program libraries. The hierarchical library unit naming may allow these inter-library references to be handled more naturally within the language, for example, by treating the library units of one program library as child units of an empty package within the referencing library. This would provide automatic name-space separation between the two libraries, since the names of the units of the referenced library would all be prefixed by a hypothetical parent package identifier. This approach would eliminate any need for global uniqueness of library unit names when two or more program libraries are (conceptually) combined in this way. We have avoided specifying standard mechanisms for such inter-program-library references, as implementations vary widely in what they choose to provide in this area. However, the universal availability of hierarchical library unit naming will ensure that a program built out of units from multiple libraries will have a natural and portable representation by using a hierarchical naming approach. S.10.5. Elaboration of Library Units Because Ada allows essentially arbitrary code to execute during the elaboration of library units, it is difficult for the user to ensure that no subprogram is called before it is elaborated. Ada 83 requires that access before elaboration be detected, and PROGRAM_ERROR raised (see RM-3.9(4-9)). This can incur significant overhead at run-time. For Ada 9X, we are addressing both the problem of controlling library unit elaboration order, and the run-time overhead of access-before-elaboration checks. First of all, the ELABORATE pragma is being made ``transitive,'' by which we mean that a pragma ELABORATE on a unit causes not only the body of that unit to be elaborated, but also causes the bodies of the units reachable from that unit's body to be elaborated. This ensures that any call performed during elaboration on a subprogram defined in the pragma-ELABORATEd unit will not result in an access-before-elaboration error. In MS-10.5.1 we define two package categorization pragmas to help minimize the overhead of access-before-elaboration checks. The pragma PURE in a package specifies that the package may not have any library-level ``state,'' and may depend only on other pure packages. Such a package requires no access-before-elaboration checking, because it can be effectively ``preelaborated'' before the program execution begins. The pragma ELABORATE_BODY in a package specifies that the body of the package must be elaborated immediately after its declaration. No intervening elaborations are permitted. This allows the compiler to know whether or not any elaboration-time code exists between a visible subprogram declaration and its body. If there is no such elaboration-time code, or it can be proved to not call the subprogram, then there is no opportunity for access-before- elaboration, and the check may be completely eliminated for the subprogram. Without this pragma, the compiler must assume that other library units might be elaborated between the elaboration of the library unit's declaration and its body, meaning that the check in a visible subprogram cannot be removed by the compiler. Further support for preelaboration is described in MS-G, Systems Programming Annex. As stated in MS-10.5, we are also studying a proposal to require that all access-before-elaboration (ABE) checking be performed statically. S.10.5.1. Library Unit Package Categorization [new] The pragmas described in MS-10.5.1 are discussed above in S.10.5. S.10.6. Program Optimization S.11. Ada 9X Exceptions S.11.1. Exception Declarations S.11.2. Exception Handlers In a fault-tolerant system, it is usually necessary to recover from exceptions and continue processing in some way. However, it is also important to be able to log all errors that occur, for future analysis. The type EXCEPTION_OCCURRENCE allows the circumstances associated with an unanticipated run-time error to be recorded in an error log to support such error analysis. Here is an example of how a task in an embedded system might log errors for further analysis: with SYSTEM; separate (SOME_PKG) task body SOME_TASK is . . . begin loop begin . . . -- Execute primary task algorithm. exception when E : others => -- Unhandled exception; log it. PUT(LOG_FILE, "Unknown error in task SOME_TASK: " & SYSTEM.EXCEPTION_NAME(E) & SYSTEM.EXCEPTION_INFORMATION(E)); . . . -- Reset data structures as necessary. end; -- Loop around to restart task. end loop; end SOME_TASK; In Ada 83, there is no way to get the identity of the exception that caused an others handler to get control. The choice parameter now allows that. An implementation may also provide other useful information in the type EXCEPTION_OCCURRENCE. For example, the current values of the machine registers might be useful in some environments; a stack trace-back might be useful in some environments. Since this information is implementation-defined, we provide a portable way to print some or all of it out: EXCEPTION_NAME and EXCEPTION_INFORMATION. Note that an implementation may choose to include some information in an exception occurrence that is not returned by EXCEPTION_NAME or EXCEPTION_INFORMATION, presuming it provides additional operations for extracting the information. S.11.3. Raise Statements [originally 11.3] S.11.4. Exception Handling S.11.4.1. Exceptions Raised During the Execution of Statements S.11.4.2. Exceptions Raised During the Elaboration of Declarations S.11.5. Exceptions Raised During Task Communication S.11.6. Exceptions and Optimization The Ada 83 RM section 11.6 defines the kinds of transformations that may be performed by an optimizing compiler. For Ada 9X, we are proposing that there be two separate compilation modes. In ``careful'' mode, no transformations that might be visible due to exception handling may be performed. Furthermore, canonical order of execution must be preserved (i.e. no ``code motion''). In default compilation mode, almost any value-preserving transformation will be allowed unless there is a lexically enclosing exception handler. Therefore, exception handlers that only dynamically enclose a computation should not depend on the order in which global effects occur. This mode is designed to allow maximum use of heavily pipelined, or fully parallel hardware. The Language Precision Team is preparing a longer report on the issues regarding exceptions and optimization, and we anticipate following their recommendations. S.11.7. Suppressing Checks S.12. Ada 9X Generic Units S.12.1. Generic Declarations The class-wide programming features of Ada 9X (see MS-3.3) will reduce the need to use generics to deal with different types derived from the same root type. However, class-wide programming does not address the important capability, only provided by generics, of defining a new data structure that is parameterized by one or more component types. It is instructive to note that the strongly-typed object-oriented programming languages C++, Eiffel, and Modula-3 all include some sort of generic or template mechanism in their latest incarnations. We have proposed three new kinds of generic formal parameters. The first is a generic formal tagged type. This allows the formal type to be the parent in a type extension (see MS-3.4.1). Because there is no direct support for multiple inheritance in Ada 9X, one way of compensating for this is by defining a generic package that extends a formal type with additional components and operations. Because type extension is only permitted for tagged types, allowing the reserved word tagged in a generic formal private type declaration makes it clear in the parameter specification that extension will be performed. The second new kind of generic formal parameter is the formal derived type. As mentioned above, we see an important role for generics in the definition of new data structures parameterized by one or more component types. For linked data structures, it is often necessary to take advantage of the structure of the components to efficiently implement the (composite) data structure. By using a generic formal derived type, the implementation of the generic can take advantage of the structure and operations of the root parent type specified for the type parameter. See section MS-12.3.6 for an example. The third new kind of generic formal parameter is the formal package. A formal package parameter matches any instantiation of a specified generic package. The generic formal package is useful for importing an entire abstraction defined by a preexisting generic package, simplifying the layering of one generic on top of another. See section S.12.1.4 for more detail on generic formal packages. In addition to the three new kinds of generic formal parameters, we now allow the explicit differentiation between formal private types that may be used to declare variables without an initializing expression, and those that may not. If a generic formal private or derived type includes a formal discriminant part of (<>), then the actual may have discriminants without defaults or unconstrained array bounds. Such generic formal types may not be used in the declaration of a variable without an initial expression, or in an uninitialized allocator. On the other hand, if a generic formal private or derived type has no formal discriminant part, then the actual must not have unconstrained array bounds, and if it has unconstrained discriminants, there must be defaults for them. This new distinction eliminates the primary source, in Ada 83, of errors where an otherwise legal instantiation is made illegal by a particular usage pattern of a formal type within the body of the generic. In other words, this distinction eliminates the major ``gap'' in the generic ``contract model'' of Ada 83. Although this new distinction is not strictly upward compatible, the fix is very straightforward. If a generic is designed so that an actual type can be an unconstrained composite type, then the corresponding generic formal should have (<>) as its formal discriminant part. On the other hand, if within the generic body it is assumed that a variable of the type can be declared without an initial expression, then the formal discriminant part must not be (<>). This distinction is clearly important, and would in any case have to be included in the commentary defining the applicability of the generic. The (<>) syntax makes a distinction that was previously only describable in commentary into a language-defined and compiler-enforced distinction. Having plugged the major gap in the Ada 83 generic contract model, we have gone further to ensure that the legality of an instantiation should never depend on the parameter usage patterns present in the generic body. In particular, we have proposed that the generic body must be checked when it is compiled while assuming the ``worst'' about the actual generic parameters. This rule ensures that a generic body may be compiled and recompiled without ever making existing instantiations illegal. It also means that instantiations may be checked for legality without ever consulting the generic body. While eliminating contract model violations associated with the generic body, we have explicitly extended the definition of the ``contract'' to include the entire generic specification. In other words, the generic specification is checked when it is first compiled assuming the ``best'' about the actual generic parameters. That is, the generic specification is legal so long as there is one generic instantiation that would be legal. When an actual generic instantiation is performed, additional checks are performed to ensure that the instance is legal; that is, that the instance conforms to the ``contract'' implied by the set of declarations occuring within the generic specification. This approach provides for maximum flexibility within the generic specification, while providing maximum independence between an instantiation and a generic body to preserve the benefits of separate compilation. S.12.1.1. Generic Formal Objects S.12.1.2. Generic Formal Types For a generic formal derived type, the primitive operations available on the type in the generic are determined by the specified parent type. Analogous to the rule for formal numeric types, the primitive operations available on an untagged formal derived type use the parent operation implementations, even if they have been overridden or hidden for the actual type. This rule is necessary for untagged types, because there is no limitation on the kinds of alterations made to the subtype or mode of the formal parameters when overriding a subprogram inherited by derivation. The overriding operation may also be made abstract (see MS-6.1). For tagged types, the primitive operations use the implementations defined for the actual type, though this is expressed for consistency in terms of the normal dispatching behavior of the operations of the parent type. For a tagged type it is possible to use the overriding definitions, because by MS-3.4.1(8), these overriding operations must be subtype conformant with the inherited one. S.12.1.3. Generic Formal Subprograms S.12.1.4. Generic Formal Packages [new] Generic formal packages are appropriate in two different circumstances. In the first circumstance, the generic is defining additional operations, or a new abstraction, in terms of some preexisting abstraction defined by some preexisting generic. This kind of ``layering'' of functionality can be extremely cumbersome if all of the types and operations defined by the preexisting generic must be imported into the new generic. The generic formal package provides a direct way to import all of the types and operations defined by an instantiation of the preexisting generic. A second circumstance where a generic formal package is appropriate is when the same abstraction is implemented in several different ways. For example, the abstraction of a ``mapping'' from a key type to a value type is very general, and admits to many different implementation approaches. In most cases, a mapping abstraction can be characterized by a key type, a value type, and operations for adding to the mapping, removing from the mapping, and applying the mapping. This represents a ``signature'' for the mapping abstraction, and any combination of types and operations that satisfy such a signature syntactically and semantically can be considered a ``mapping.'' A generic package can be used to define a signature, and then a given implementation for the signature is established by instantiating the signature. Once the signature is defined, a generic formal package for this signature can be used in a generic formal part as a short-hand for a type and a set of operations. Continuing the ``mapping'' example from above, one could define a generic package MAPPING that defines the signature of a mapping, and then other generics can be defined with a formal package parameter. Example of generic package as signature and in a generic formal package parameter: generic -- Define signature for a Mapping type MAPPING_TYPE is limited private; type KEY is limited private; type VALUE is limited private; with procedure ADD_PAIR(M : in out MAPPING_TYPE; K : in KEY; V : in VALUE); with procedure REMOVE_PAIR(M : in out MAPPING_TYPE; K : in KEY; V : in VALUE); with procedure APPLY(M : in out MAPPING_TYPE; K : in KEY; V : out VALUE); package MAPPING is end; -- Define a generic that takes a MAPPING instance as a parameter generic with package SOME_MAPPING is new MAPPING(<>); with procedure DO_SOMETHING_WITH_VALUE( V : SOME_MAPPING.VALUE) procedure DO_SOMETHING_WITH_KEY(K : SOME_MAPPING.KEY); procedure DO_SOMETHING_WITH_KEY(K : SOME_MAPPING.KEY) is V : SOME_MAPPING.VALUE; begin -- Translate key to value, and then do something with value SOME_MAPPING.APPLY(K, V); DO_SOMETHING_WITH_VALUE(V); return; S.12.2. Generic Bodies In Ada 83, each instantiation of a generic containing an exception declaration results in the creation of an additional unique exception. For Ada 9X, we are preserving this rule for exceptions declared in the specification of a generic package, but allowing exceptions declared in the body of a generic package to not result in unique exceptions in each instantiation. By relaxing this requirement, we eliminate one of the most difficult aspects of supporting code sharing between instantiations, while creating only an extremely minor upward incompatibility. To notice whether an exception declared in a generic body is replicated or shared in each instantiation, a contrived situation like the following must be constructed: generic package GEN is -- Generic package with procedure -- that propagates exception declared in its body. procedure RAISE_EXCEP; function EXCEP_IS_SHARED return BOOLEAN; end GEN; package INST is new GEN; package body GEN is -- Body of generic that has visibility on -- some instantiation of the generic. BODY_EXCEPTION : exception; procedure RAISE_EXCEP is -- Raise exception declared in body. begin raise BODY_EXCEPTION; end RAISE_EXCEP; function EXCEP_IS_SHARED return BOOLEAN is -- Check if exception declared in body is -- same as the one raised by INST.RAISE_EXCEP. begin INST.RAISE_EXCEP; exception when BODY_EXCEPTION => -- Same exception, must be shared. return TRUE; when others => -- Different exception, must be replicated. return FALSE; end EXCEP_IS_SHARED; end GEN; package ANOTHER_INST is new GEN; EXCEP_IS_SHARED : BOOLEAN := ANOTHER_INST.EXCEP_IS_SHARED; -- call function to check if body exception is shared. As illustrated in the above example, to notice whether the exception declared in a generic body is replicated or shared, the following contrived combination must exist: - The exception must be propagated out of the generic body by some visible operation; - The generic body must depend on one of its own instantiations, and it must call an operation of that instantiation that propagates the exception declared in the body; - The above call must be in a frame with a handler for the exception declared in the body. Given the above, the benefit from simplifying generic code sharing outweighs the possible upward incompatibility. Note that we are not requiring sharing of an exception declared in the body of a generic, simply allowing it. (Requiring sharing might make the macro expansion of a generic body more complicated for existing implementations.) S.12.3. Generic Instantiation S.12.3.1. Matching Rules for Formal Objects S.12.3.2. Matching Rules for Formal Private Types The matching rules for generic formal discriminated types (those declared with a (<>) discriminant part) are needed to avoid contract model violations, as explained in S.12.1. S.12.3.3. Matching Rules for Formal Scalar Types S.12.3.4. Matching Rules for Formal Array Types S.12.3.5. Matching Rules for Formal Access Types The rules for matching of general access types are constructed to avoid contract model violations in this area. For example, the compiler knows whether a given formal access type is access-to-constant, and must do the appropriate error checking on that basis. S.12.3.6. Matching Rules for Formal Derived Types [new] Formal derived types allow the use of known operations of the type in the body of the generic. Thus, to fulfill the contract, the actual type is required to be in the same derivation tree. Tagged class-wide types must be considered to have an unknown set of discriminants, because extensions can replace the discriminants with new (and often more) discriminants. S.12.3.7. Matching Rules for Formal Subprograms [originally 12.3.6] S.12.3.8. Matching Rules for Formal Packages [new] S.12.4. Example of a Generic Package S.13. Ada 9X Representation Clauses and Implementation-Dependent Features S.13.1. Representation Clauses S.13.2. Attribute Definition Clauses [originally Length Clauses] Ada 83's length clauses are replaced with attribute definition clauses, allowing us to use this syntax to define various attributes (as opposed to just ``lengths''). Various attributes are defined to be ``user-specifiable;'' these are the ones that are allowed in attribute definition clauses. The user-specifiable attributes include SIZE, STORAGE_SIZE, and SMALL, as in Ada 83. Several of the user-specifiable attributes are allowed for subtypes and objects, in addition to types. For the SIZE, this provides the ability to specify the exact size of an object. (Ada 83's SIZE specification can only specify a minimum.) For STORAGE_SIZE, this provides the ability to have different storage sizes for distinct tasks of the same task type. It is important that we allow implementations to define new attributes as user-specifiable, because we use this functionality heavily in the Specialized Needs Annexes. (Those Annexes can only contain features that are allowed as implementation-defined features in any case.) The new ALIGNMENT attribute is important on many modern machines where controlling the alignment is important for efficiency. The only way to control alignment in Ada 83 is to use a record type, which is inappropriate if the sensible type is, for example, a scalar. For simplicity reasons, it is important that each attribute definition clause refer to an actual attribute. For example, it would be confusing to allow ``for T'COMPONENT_SIZE use ...;'' but to disallow the use of the COMPONENT_SIZE attribute as a query. Some attributes may not seem of obvious use in non-generic code, but they can be essential when trying to implement a generic unit. S.13.3. Enumeration Representation Clauses S.13.4. Record Representation Clauses Tagged types can be extended with new components. Values of the extended type can be viewed as being of the parent type. Therefore, in order to ease the generation of efficient code, it is important that the layout of the parent type be the same as the layout of the parent part of a derived type. In order to allow block copies and block comparisons when viewing an object or value as being of the parent type, we disallow the new components of the extension from being ``mixed in'' with the components inherited from the parent. S.13.5. Address Clauses In Ada 83, implementations are allowed to support non-static addresses in address clauses. Many, in fact, do so. For Ada 9X, we require that all implementations support non-static addresses for ``external'' objects and subprograms, in order to enhance uniformity across implementations. This allows an address clause to act as a portable alternative to using unchecked conversion of an address to an access value. S.13.5.1. Interrupts S.13.6. Change of Representation S.13.7. The Package SYSTEM Implementations do not uniformly support pragmas SYSTEM_NAME, STORAGE_UNIT, and MEMORY_SIZE. In fact, it is not even clear what they should mean. For example, in most implementations, it does not make sense to change the number of bits in a storage unit, and even if it did, it would not be sufficient to make only package SYSTEM obsolete; clearly all generated code depends on this value. Therefore, we no longer require that implementations support these pragmas. Of course, implementations that already support them with some particular meaning can continue to support them (as implementation-defined pragmas) for upward compatibility. The corresponding named numbers in package SYSTEM, on the other hand, are quite useful as queries; we do not propose to change them. We add several declarations to package SYSTEM to support address arithmetic and comparison. Implementations are already allowed to provide such declarations, and most do; we are merely making such support more uniform. Of course, the detailed semantics of such operations is implementation-defined. On some machines, address arithmetic makes no sense in certain cases; the implementation can raise an exception in such cases. S.13.7.1. System-Dependent Named Numbers S.13.7.2. Representation Attributes S.13.7.3. Representation Attributes of Real Types S.13.8. Machine Code Insertions S.13.9. Interface to Other Languages It is very important for Ada 9X to be able to effectively interface to systems written in other programming languages. For example, the success of Ada 9X depends in part on its ability to cleanly support interfaces to such systems as X windows and POSIX. Therefore, we enhance Ada 83's pragma INTERFACE with the following capabilities: - The ability to interface to external objects. (Ada 83 only supports external subprograms.) - The ability to call Ada subprograms from other languages. (Ada 83 only supports calls in the other direction.) - The ability to communicate with external languages via access-to- subprogram types. - The ability to specify ``link names'' where appropriate. Most Ada 83 implementations supported such an ability; it is beneficial to users that it be standardized. S.13.10. Unchecked Programming S.13.10.1. Unchecked Storage Deallocation S.13.10.2. Unchecked Type Conversions S.13.10.3. Unchecked Access Value Creation S.14. Ada 9X Input-Output S.14.1. External Files and File Objects S.14.2. Sequential and Direct Files S.14.2.1. File Management S.14.2.2. Sequential Input-Output Opening a file for appending is such a useful capability, and is so common in other languages, that we have added a new file mode for appending to both SEQUENTIAL_IO and TEXT_IO. S.14.2.3. Specification of the Package Sequential_IO S.14.2.4. Direct Input-Output Append mode is unnecessary for DIRECT_IO, because direct files already support arbitrary positioning, and for these files, there is nothing particularly special about positioning to the end of the file. S.14.2.5. Specification of the Package Direct_IO S.14.3. Text Input-Output S.14.3.1. File Management S.14.3.2. Default Input and Output Files S.14.3.3. Specification of Line and Page Lengths S.14.3.4. Operations on Columns, Lines, and Pages S.14.3.5. Get and Put Procedures S.14.3.6. Input-Output of Characters and Strings To ease the writing of simple programs that do input/output, we are considering making some minor changes to TEXT_IO. In particular, we are considering a GET_LINE function (which is more convenient than the GET_LINE procedure, because the caller does not have to know the maximum size of the line). and changing STRING to STRING'CLASS to allow easier use of the Ada 9X class capabilities. S.14.3.7. Input-Output for Integer Types We have defined GET and PUT routines for the integer class in order to allow simple input/output for integers of any type without having large numbers of generic instantiations. Calling these routines GET and PUT would not be upward compatible -- if existing code has a use clause for both TEXT_IO and an INTEGER_IO instance, GET and PUT calls in such code could become ambiguous; hence the names GET_INT and PUT_INT. S.14.3.8. Input-Output for Real Types The rationale for this section is analogous to that of the previous section. S.14.3.9. Input-Output for Enumeration Types S.14.3.10. Specification of the Package Text_IO S.14.4. Exceptions in Input-Output S.14.5. Specification of the Package IO_Exceptions S.14.6. Low Level Input-Output The semantics of LOW_LEVEL_IO are so implementation dependent that we decided to remove the discussion of this package. It was rarely supported in Ada 83, and most programs used other methods for performing low-level input and output. S.14.7. Stream Input-Output [new] S.14.7.1. T'READ and T'WRITE [new] The 'READ and 'WRITE operations are intended for use in passing data across a network, for use with SEQUENTIAL_IO, and for writing user-defined input/output packages. The definitions of T'READ and T'WRITE for scalars and access types are meant to bypass concerns about register size, optimal size of stand-alone objects, etc. For tagged universal types, the string that is used to represent the tag is different from the representation of the tag in memory. The value does not vary across different programs that use the type, so long as the type declaration is not recompiled. The T'READ operation is expected to look up the string in a table at run-time to find the corresponding type tag; it can then raise an exception if the string is not found (implying that the type was not linked into the program that executed the T'READ). S.14.7.2. Predefined T'WRITE and T'READ [new] The predefined 'WRITE and 'READ operations are not guaranteed to be useful across heterogeneous networks, because some implementation freedom is allowed. However, since the user can override these operations, it is possible to construct programs that communicate across heterogeneous networks; it is up to the user to choose an appropriate representation. S.14.7.3. Validation on T'READ [new] S.14.7.4. T'WRITE and T'READ for Tagged Class-Wide Types [new] S.14.7.5. The Package STREAM_SUPPORT [new] S.14.7.6. Heterogeneous Input-Output [new] S.14.8. Example of Input-Output U. Analysis of Ada 9X Upward Compatibility/Consistency This Appendix examines the proposals of Mapping Specification 4.0 with respect to upward compatibility and consistency with Ada 83. First, here is some terminology: Upward Compatible A proposed change is upward compatible if the semantics of all legal Ada 83 programs are unaffected by the change. In particular, all legal Ada 83 programs remain legal, with the same meaning. Upward Consistent A proposed change is upward consistent if the semantics of all legal Ada 83 programs that remain legal are unaffected by the change. Some legal Ada 83 programs may become illegal, but those that remain legal have the same meaning. Upward Consistent if no Constraint Errors (UCINCE) A proposed change to a given feature is ``upward consistent if no constraint errors'' (UCINCE) if the semantics of all legal Ada 83 programs that do not raise a constraint error relating to the feature and that remain legal, are unaffected by the change. Some legal Ada 83 programs may become illegal, and some that raised constraint error may no longer do so if a meaningful result can be produced. Upward Inconsistent A proposed change is upward inconsistent if the semantics of some legal, correct Ada 83 programs are affected by the change. In particular, the program may now raise a predefined exception when it didn't in the past, or it may produce a different result. We will not discuss changes that are upward compatible any further. For all of the others, the relevant questions include: (a) How common are programs that will have the upward incompatibility? We give examples if necessary to illustrate the problem. (b) How easy is the work-around? We give examples if necessary to illustrate the approach. (c) Is there a work-around that works identically in both Ada 83 and Ada 9X? (d) Are all instances of upward inconsistency detectable statically (i.e. is it a ``warnable'' upward inconsistency)? (e) How serious is the change in effect estimated to be for a typical instance of the inconsistency? Only (c) and (d) are objective questions, (a), (b), and (e) depend on estimates, and so are hence subject to discussion and debate, and would benefit from systematic surveys of existing code. Please note we are trying to be exhaustive here, so we are mentioning even ``marginal'' cases. At the end we will try to summarize what we see as the ``major'' cases. Note also that some of these upward incompatibilities relate to features that may not be included in the ultimate Ada 9X, or that may appear in a different form that is more nearly upward compatible. In some cases we discuss possible changes that might improve the upward compatibility of a feature. Finally, note that we are not considering cases where Ada 9X limits implementation freedom relative to Ada 83, unless it is clear that the newly required behavior is noticeably different from the typical implementation behavior. We may produce a separate document to discuss implementation freedom issues at a later date. U.1. Incompatibilities 1. MS-2;4.0: New reserved words -- Upward Consistent (a) We estimate relatively few collisions with the new reserved words (aliased, protected, requeue, tagged, until). (b) The work-around is straightforward -- replace the identifier with a new one; recompile and see if there are any overloading problems; iterate if necessary. (c) The work-around works identically in Ada 83 and Ada 9X (d) N/A, since the change is upward consistent, and by definition any problems are detected at compile-time and the program is rejected. (e) N/A 2. MS-3.3.2;4.0: Subtype accuracy constraints removed from the language -- Upward Consistent (a) We estimate relatively few uses of subtype accuracy constraints. (b) The work-around is straightforward -- remove the accuracy constraint from the object, subtype, or derived type declaration; for a derived type, it may be preferred to define a new real type presuming there are no user-defined derivable subprograms; scan for uses of subtype-specific attributes whose value will now reflect the base-type attributes (see also changes to numeric model, below). (c) The work-around works identically in Ada 83 and Ada 9X (d) All cases rejected at compile-time (e) N/A 3. MS-3.3.3;4.0: A derived type inherits all operations of the parent type declared immediately in the same list of declarations as the parent type -- Upward inconsistent (a) We estimate that explicit type derivation, when the parent type has user-defined derivable operations, is relatively rare in Ada 83. Furthermore, we estimate that deriving from a type in the same list of declarations as the parent type declaration is even rarer. Finally, we estimate that deriving from a parent type in the same list of declarations, after one of the parent's derivable operations has been overridden, with the expectation that one will get the non-overridden operation is even rarer still. (b) The work-around needed to get the non-overridden operations (presuming that is the intent) is straightforward: If the parent type is a derived type, derive from its parent. If the parent type is a numeric, array, or general access type, define a new numeric/array/general access type, with the same characteristics as the former parent type. (Numeric, array, and general access types are explicitly interconvertible like derivatives so long as they have compatible characteristics). If the parent type is some other kind of type, define a third type, make it the parent of the old parent type and the old derived type. (Note that this third method actually works for all types.) (c) The work-around works identically in Ada 83 and Ada 9X. (d) All potential inconsistencies are warnable. (e) The effect of the inconsistency is that the derived type uses the direct parent's definition of an operation, rather than the predefined operation or its ``grand parent's.'' 4. MS-3.3.3;4.0: A type derivation is illegal if it precedes the definition of the last primitive operation of the parent type -- Upward Consistent [Note: the restriction against adding primitive operations after a body is being dropped -- it was too restrictive. The wording of the remaining restriction against ``early'' derivation is inverted to place the ``blame'' on the type derivation rather than the definition of the primitive operation.] (a) We estimate that explicit type derivation, when the parent type has user-defined derivable operations, is relatively rare in Ada 83. Furthermore, deriving from a type in the same list of declarations is even rarer. Note that we are considering limiting this restriction to tagged types, in which case the (untagged) type derived before the last primitive operation has been defined would not inherit the operations defined later. This restriction would therefore not be an upward incompatibility. It would also open up another work-around for the upward inconsistency associated with inheriting operations from type defined outside a visible part, namely move the type derivation up to immediately following the parent's type declaration. (b) The work-around is straightforward: Move the type derivation down to after the definition of the last primitive operation. Alternatively, the the above-mentioned work-around of introducing a third type to act as the parent of both previous types solves the problem, if the intent is to not inherit the parent type's operations. (c) A work-around exists that works identically in Ada 83 and Ada 9X. (d) and (e) N/A 5. MS-3.5.2;4.0: CHARACTER now has 256 positions -- Upward Inconsistent (a) We estimate a medium number of cases where legal programs will become illegal; we estimate a small number of legal programs that will remain legal but have a different effect. An example of an inconsistent program is one that refers to CHARACTER'LAST, since this will now have a position number of 255 instead of 127. (b) The work-around is straightforward, though not trivial -- Decide how to handle characters in the ``upper half''; Add others clause to aggregates and case statements based on CHARACTER, or appropriate specific choices for interesting upper-half characters. (c) There are work-arounds that will work identically in Ada 83 and Ada 9X presuming there are no upper-half characters in the input. (d) The upward inconsistencies are warnable, since they are generally related to uses of CHARACTER'LAST. (e) We estimate that the effect of the upward inconsistencies is relatively mild. Furthermore, most programs that have upward inconsistencies will also have upward incompatibilities, so they will be rejected at compile-time anyway. 6. MS-3.5.2;4.0: All character literals are now overloaded on both WIDE_CHARACTER and CHARACTER -- Upward consistent (a) We estimate the number of cases that rely on the type uniqueness of characters to be very small. Here is an example of one: if 'a' = 'b' then ... This will resolve in Ada 83, but be ambiguous in Ada 9X. Note that we are not proposing to add WIDE_CHARACTER (or WIDE_STRING) operations to TEXT_IO, but rather put such operations in a separate WIDE_TEXT_IO package. Therefore, the following will remain unambiguous: use TEXT_IO; ... PUT('a'); ... PUT("hello"); (b) The work-around is simple: Add a type qualification for CHARACTER, for example: if CHARACTER'('a') = 'b' then ... (c) The work-around will work identically in Ada 83 and Ada 9X (d) and (e) N/A 7. MS-3.5.2;4.0 and elsewhere: New identifiers in package STANDARD -- Upward inconsistent (a) We estimate very few collisions with names added to package STANDARD; we estimate near zero number of cases where a program with a collision would be legal and inconsistent. For example, here is a case of an upward inconsistency: package MINE is type WIDE_CHARACTER is ('a', 'b'); end MINE; with MINE; use MINE; with TEXT_IO; procedure TROUBLE is X : WIDE_CHARACTER := 'a'; -- In Ada 83, this uses MINE.WIDE_CHARACTER. -- In Ada 9X, STANDARD.WIDE_CHARACTER hides -- the use-visible MINE.WIDE_CHARACTER begin if WIDE_CHARACTER'POS(X) /= 0 then TEXT_IO.PUT_LINE("Inconsistent!"); else TEXT_IO.PUT_LINE("Consistent"); end if; end TROUBLE; (b) The new identifiers currently proposed for STANDARD are: - WIDE_CHARACTER - WIDE_STRING - INT_CLASS (ROOT_INTEGER_CLASS in our more recent thinking) - REAL_CLASS (ROOT_REAL_CLASS in our more recent thinking) - TASK_CLASS (ROOT_TASK_CLASS in our more recent thinking) (c) The work-around is straightforward: Pick a different identifier and recompile (i.e., handle this similarly to a new reserved word). (d) The work-around will work identically in Ada 83 and Ada 9X (e) The potential upward inconsistencies are warnable, by flagging any declaration of an entity with a name that appears in Ada 9X STANDARD but not in Ada 83 STANDARD. (f) We estimate the effect of any undetected inconsistencies to be relatively mild. Furthermore, as mentioned above, we expect that essentially all programs with collisions will be rejected at compile time, given the extremely small chance that the identifier in STANDARD is a legal stand-in for the previously use-visible identifier. 8. MS-3.5.4;4.0: Exceeding the FIRST and LAST attributes of an integer base type need not raise CONSTRAINT_ERROR -- UCINCE (a) We estimate very few programs now rely on the raising of CONSTRAINT_ERROR on assignment to variables of an integer base type, so long as the correct answer is produced in the end. RM 11.6 already grants the freedom to perform evaluations in greater range than the base type. (b) The work-around is straightforward -- Define and use a constrained subtype rather than an unconstrained integer base type. (c) The work-around works identically in Ada 83 and Ada 9X (d) The potential upward inconsistencies are warnable, since the implementation knows whenever it has allocated a ``wider'' register or storage cell for a variable of an integer base type. (e) The effect of the upward inconsistency is to avoid a constraint error and continue further in an algorithm with a value outside of BASE'FIRST .. BASE'LAST. 9. MS-3.5.6;4.0: Exceeding the FIRST and LAST attributes of a real base type need not raise CONSTRAINT_ERROR -- UCINCE (a) .. e) See (8) above. 10. MS-3.5.6;4.0: A range constraint on a real type (after its declaration) is a forcing occurrence for the type -- Upward consistent (a) We estimate few programs specify rep clauses for real types after defining constrained subtypes or derived types of them. (b) The work-around is very simple -- Move the rep clause to before the declaration of constrained subtypes/derived types (c) The work-around works identically in Ada 83 and Ada 9X. (d) and (e) N/A 11. MS-3.5.9;4.0: Default choice for small need not be a power of 2 -- Upward inconsistent [Note: There is a missing section header for Fixed Point Types.] (a) We estimate relatively few programs contain a delta that is not a power of 2 and have no rep-clause for small. (b) The work-around is very simple -- Change the delta to equal the desired (binary) small. (c) The work-around will work identically in Ada 83 and Ada 9X. (d) This upward inconsistency is warnable. (e) The effect of the upward inconsistency will likely be an improvement in accuracy, and a decrease in performance. 12. MS-3.7.3;4.0: String literals now overloaded on STRING and WIDE_STRING -- Upward consistent (a) We estimate very few programs depend on the resolution of the type of a string literal independent of context. Here is an example that is legal in Ada 83 but would be ambiguous in Ada 9X: if "abc" = "abc" then ... (b) The work-around is simple: Use a type qualification for type STRING. (c) The work-around works identically in Ada 83 and Ada 9X (d) and (e) N/A 13. MS-4.5.3;4.0: Low bound of catenation always same as low bound of left operand -- Upward inconsistent (a) We estimate that extremely few programs take advantage of the catenation of a null left operand taking its low bound from the right operand. Note that in many cases, the left operand of a concatenate cannot be null (because it is a non-null string literal or the result of IMAGE). In other cases, the left operand and right operand have the same low bound (e.g., IMAGE and STRING literals always have a low bound of 1. In other cases, sliding is performed on the result of the catenate, making the bounds irrelevant. (b) The work-around is straightforward: Define an operation SLOW_CONCAT that mimics the Ada 83 low bound calculation. Use it where it is possible that the left operand might be null and its low bound might be different from the low bound of the right operand. (c) The work-around will work identically for Ada 83 and Ada 9X. (d) Any potential upward inconsistency is warnable. (e) The effect of the upward inconsistency is to produce an array result with different bounds, but the same length. Because sliding happens in so many contexts, and RANGE is used in others, this will rarely cause a problem. 14. MS-4.5.7;4.0: Accuracy model simplified and moved to annex -- Upward Consistent (presuming EPSILON/SMALL/LARGE renamed to MODEL_EPSILON/SMALL/LARGE as anticipated) (a) We estimate that relatively few programs (not including ACVCs) reference model attributes. (b) The work-around is straightforward: The Ada 83 model attributes are all a function of F'MANTISSA (also called ``B''), which is in turn a function of F'DIGITS. The references to the Ada 83 model attributes may be replaced with their respective formulas in terms of F'DIGITS. (c) The work-around works identically in Ada 83 and Ada 9X. (d) and (e) N/A. 15. MS-4.6;4.0: Implicit array subtype conversion (sliding) performed in more contexts -- UCINCE (a) This change is upward compatible for programs that do not currently raise CONSTRAINT_ERROR due to an array bounds mismatch. The only effect of this change is that where a program before would raise constraint error if the bounds mismatched, it will now ``slide'' the array bounds. No constraint error will be raised if the array lengths match. We estimate that no programs take advantage of the current constraint error as part of their ``correct'' behavior. (b) There is no apparent need for a work-around. However, if there is an interest in checking bounds prior to sliding, an explicit subtype membership test may be inserted. (c) An explicit subtype membership test will work identically in Ada 83 and Ada 9X. (d) All possible inconsistencies are warnable. (e) We estimate the effect of the inconsistency to be very minor, since the value after sliding must still have the correct length. 16. MS-7.4.5;4.0: Function return of limited type defers finalization -- Upward Inconsistent (a) We estimate that extremely few, if any, functions that return limited types have local tasks that are still active when the return statement is reached. In Ada 83, the function will wait after calculating the return value, but before returning to the caller, for the local task to terminate. In Ada 9X the function will return immediately, and then the caller will wait for the task to terminate after finishing using the return value. (b) If any such functions do in fact exist, the local task can presumably be declared in a nested block to ensure that it is terminated prior to the function return. However, as mentioned above, we doubt that any such functions exist (outside of the ACVC suite). (c) A nested block will work identically in Ada 83 and Ada 9X. (d) All possible inconsistencies are warnable. (e) We estimate the effect of the inconsistency to be very minor, first because we doubt that any such functions exist, and second, because in both Ada 83 and Ada 9X the wait for local task termination occurs after evaluating the return value. So there is no way to take advantage of the terminated state of the local task, even in Ada 83. 17. MS-8.3;4.0: All primitive operators and character literals are ``primitively visible'' when not otherwise directly visible -- Upward Consistent (a) We estimate that very few programs will become ambiguous due to the primitive visibility of operators and character literals. To become ambiguous, the program must use a non-primitive (and hence user-defined) operator that is only use-visible, in a way that is ambiguous with some primitive (predefined) operator. Note that a non-primitive operator that is a rename of a primitive operator hides the primitive operator, when the renaming is use-visible. Therefore, an ``operators'' package containing renames of primitive operators may be ``use''d without creating ambiguity with the corresponding primitive operators. (b) The work-around is straightforward: Use selected component notation for the non-primitive operator, or an explicit rename. In Ada 9X, one may explicitly ``hide'' the predefined operator with ``is <>'' if it should never be visible (see MS-6.1;4.0). (c) The selected-component or explicit rename work-around works identically in Ada 83 and Ada 9X. (d) and (e) N/A 18. MS-8.7;4.0: Preference for root (universal) numeric operators, rather than against implicit conversion -- Upward consistent (a) We estimate that extremely few programs will become ambiguous due to the revised preference rule. Here is an example: function "+"(Left, Right : My_Integer) return Integer; ... Some_Integer(3 + 5) ... The operand ``3 + 5'' would resolve in Ada 83 to universal integer, even though this weird "+" is immediately visible. In Ada 9X, this would be ambiguous because this weird "+" is not a primitive operator corresponding to a universal-int operator. (b) The work-around is straightforward: Insert one or more type or package qualifications to remove ambiguity. (c) The work-around works identically in Ada 83 and Ada 9X. (d) and (e) N/A 19. MS-10.1;4.0: Body required for library unit package -- Upward Consistent (a) We estimate that relatively many library unit packages exist that lack a body. (b) The work-around is straightforward: At the end of the source file containing the package spec, including a (null) package body. (c) The work-around works identically in Ada 83 and Ada 9X (d) and (e) N/A 20. MS-10.5;4.0: Pragma ELABORATE applies to the closure of bodies reachable from the specified library unit -- Upward Consistent (a) We estimate that a small number of programs will be rejected because of an indirect circularity of pragma ELABORATEs. Note we are considering other approaches to improving control over elaboration order. One simple alternative is to allow indirect circularities, in which case an implementation- defined order for the bodies in the cycle is chosen. Another alternative is to rely on ELABORATE_BODY to solve most of the problems, and leave ELABORATE as is in Ada 83. A third alternative is to go to a static access-before-elaboration checking approach, so that PROGRAM_ERRORs due to ABEs will never occur, and the programmer need not insert pragma ELABORATEs explicitly to prevent them. (b) The work-around involves determining the desired order of elaboration, and moving the pragma ELABORATEs to constrain the ordering without creating an indirect circularity. (c) The rearrangement of the pragma ELABORATEs will be legal for Ada 83, but may be underconstraining depending on the linker implementation. (d) and (e) N/A 21. MS-12.1;4.0: Assume the worst when checking a generic body -- Upward Consistent (a) Not including the issue of unconstrained actual types (see next item), we estimate that extremely few programs currently take advantage of the other Ada 83 contract model violations. In Ada 9X, the ``assume the worst'' model for generic bodies require the following: - a generic formal scalar object of mode IN OUT must be assumed to be of a non-static subtype, and hence requires an others clause when used as a case expression. - an array-type formal parameter of a generic formal subprogram must be assumed to be unconstrained, precluding the use of an others choice in an array aggregate used as an actual parameter. In Ada 83, these cases are still under ARG discussion (see AI-847 and AI-878). (b) The work-around is straightforward: Add an others clause to the case statement, and use a subtype-qualified aggregate to the call. Note this implies that the formal subtype is the same as the actual subtype. If not, one may have to pass the bounds for the actual subtype as separate generic parameters. (c) The work-arounds work identically in Ada 83 and Ada 9X. (d) and (e) N/A 22. MS-12.1.2;4.0: ``(<>)'' required if actual type is unconstrained composite without defaults -- Upward Consistent (a) A modest number of generic units are designed to handle actual type parameters that are unconstrained composite types without defaults. In Ada 83, such generics must never declare variables or components of the formal type. (b) The work-around is straightforward: If the generic unit can handle unconstrained composite types without defaults as the actual type for a given formal private parameter, add ``(<>)'' to the specification of that formal private type. (c) The work-around is not legal Ada 83 syntax. It must be added in when moving code to an Ada 9X compiler. A structured Ada 83 comment could be recognized by a tool that could perform the transformation in either direction automatically. (d) and (e) N/A 23. MS-12.2;4.0: Exceptions declared in generic bodies need not be replicated for each instantiation -- Upward inconsistent (a) We estimate that there are no existing programs (other than ACVCs) that will show upward inconsistencies due to this change. As explained in the section-by-section rationale, a program can only notice this change if it has a generic body that depends on one of its instances, and the instance propagates the exception outside of its scope, and the generic body calls a subprogram of the instance and tries to catch the local exception. (b) The work-around is straightforward (though as stated above, we estimate that there are no programs that will need it): Move the declaration into the private part of the generic spec (presuming it is a generic package) or reduce the coverage of the exception handler for the local exception so that it doesn't include the call on the offending instance subprogram. (c) The work-around will work identically on Ada 83 and Ada 9X. (d) Any potential inconsistencies are warnable. (e) The effect of the inconsistency is very mild, namely that an exception propagated by the subprogram of the instance is caught by the handler in the generic body. 24. MS-13.7;4.0 and elsewhere: New identifiers added to language-defined packages -- Upward consistent (a) We anticipate that very few programs will collide with the new identifiers added to the various language-defined packages (e.g. SYSTEM, SEQUENTIAL_IO, TEXT_IO, etc.). The collisions will be noticeable when the language-defined package is ``use''d along with some user-defined package. The rules of use-visibility ensure that these changes will be upward consistent. Note that we could minimize the upward incompatibility by moving some of these new declarations into child packages of the language-defined packages. For example, we could put the declarations of STORAGE_OFFSET, STORAGE_ELEMENT, and STORAGE_ARRAY into a new child of SYSTEM, such as SYSTEM.ADDRESS_OPERATIONS. (b) The work-around is straightforward: Change the name of the user-defined entity, or qualify the references to the identifier with a package prefix or a type name. (c) The work-arounds will work identically in Ada 83 and Ada 9X. (d) and (e) N/A 25. MS-14.2.2;4.0 and MS-14.3.1;4.0: APPEND_FILE added to FILE_MODE enumeration -- Upward Inconsistent (a) We anticipate that a modest number of programs will be made illegal by this change, and an extremely small number will remain legal but produce inconsistent results. Programs with case statements or arrays indexed by FILE_MODE will likely become illegal, unless they use an others choice. Program that depend on the position number of FILE_MODE'LAST, while remaining legal, can show inconsistencies. (b) The work-around is straightforward: Add an others or APPEND_FILE choice to array aggregates and case statements with an appropriate value or alternative for appending. (c) It is possible to write work-arounds that will work identically in Ada 83 and Ada 9X. (d) All inconsistencies are warnable. (e) The effect of the inconsistency is presumably mild, as it relates only to the value of FILE_MODE'LAST. U.2. Summary of Incompatibilities As we have seen, all potential upward inconsistencies are warnable, though in some cases, upward consistent uses may also be flagged. All of the upward inconsistencies we estimate to correspond to rare situations, except perhaps things relating to CHARACTER. The ``major'' cases of potential upward incompatibility (i.e., will occur in a modest number of programs) we estimate to be: - Additional reserved words; - Changes to CHARACTER; - Requirement that library unit packages have bodies; - Fixing the contract model with respect to unconstrained composite actual types (i.e. the ``(<>)'' syntax); - APPEND_FILE addition. In all of these cases, the work-arounds are relatively straightforward, and we estimate that the benefits of the change outweigh any potential inconvenience. U.3. Note on NUMERIC_ERROR We have made no mention of NUMERIC_ERROR above. It seems best to eliminate NUMERIC_ERROR from STANDARD in Ada 9X. Other alternatives (like making it a rename of CONSTRAINT_ERROR) will probably break just as many programs, and leave a ``wart'' of complexity in the language for no obvious reason. In any case, if we do decide to remove NUMERIC_ERROR it will be upward ``inconsistent'' to do so (see above for definition), but in a particularly convenient way for transition: package MY_EXCEPTIONS is NUMERIC_ERROR : exception; -- Only use-visible in Ada 9X end MY_EXCEPTIONS with MY_EXCEPTIONS; use MY_EXCEPTIONS; with TEXT_IO; procedure MAIN is begin . . . exception when NUMERIC_ERROR | CONSTRAINT_ERROR => -- This would refer to MY_EXCEPTIONS.NUMERIC_ERROR in -- Ada 9X, and STANDARD.NUMERIC_ERROR in Ada 83, if we -- were to eliminate NUMERIC_ERROR from STANDARD. TEXT_IO.PUT_LINE("Overflow occurred"); end MAIN; By creating such a package, one can survive the removal of NUMERIC_ERROR without further source changes. Of course if your Ada 83 compiler's RTS already has been switched to always raise CONSTRAINT_ERROR instead of NUMERIC_ERROR, then one could simply delete all references to NUMERIC_ERROR and be done with it. References [Abadi 91] M. Abadi, L. Cardelli, B. Pierce, and G. Plotkin. Dynamic Typing in a Statically Typed Language. Transactions on Programming Languages and Systems 13(2), April, 1991. [ANSI 83] Reference Manual for the Ada Programming Language ANSI/MIL-Std-1815a edition, 1983. [Atkinson 88] C. Atkinson, T. Moreton, A Natali (editors). The Ada Companion Series: Ada For distributed systems. Cambridge University Press, 1988. [Bardin 89] B. M. Bardin and C. J. Thompson. Composable Ada Software Components and the Re-Export Paradigm. Ada Letters VIII(1), 1989. [Barnes 81] J. P. Barnes. Programming in Ada. Addison-Wesley, London, 1981. [Birtwistle 73] G. M. Birtwistle, O-J Dahl, B Myhrhaug, K. Nygaard. SIMULA Begin. Auerbach, Philadelphia, PA, 1973. [Booch 86] G. E. Booch. Object-Oriented Development. IEEE Transactions on Software Engineering SE-12(2), February, 1986. [Booch 87] G. E. Booch. Software Components with Ada. Benjamin/Cummings, Menlo Park, CA, 1987. [Brinch-Hansen 73] P. Brinch-Hansen. Concurrent Programming Concepts. Computing Surveys 5(4):224-245, 1973. [Brosgol 89] B. Brosgol. (column). ALSYS News , Fall, 1989. [Burger 87] Burger, T.M. and Nielsen, K.W. An Assessment of the Overhead Associated With Tasking Facilities and Tasking Paradigms in Ada. Ada Letters VII(1):49-58, 1987. [Burns 87] A. Burns, A. M. Lister, and A. Wellings. Lecture Notes in Computer Science: A Review of Ada Tasking. Springer-Verlag, Berlin, 1987. [Burns 89] A. Burns and A. Wellings. International Computer Science Series: Real-Time Systems and Their Programming Languages. Addison-Wesley, Reading, Massachusetts, 1989. [Cardelli 85] L. Cardelli and P. Wegner. On Understanding Types, Data Abstraction, and Polymorphism. Computing Surveys 17(4):471-522, December, 1985. [Cohen 90] S. Cohen. Ada Support for Software Reuse. Technical Report SEI-90-SR-16, Software Engineering Institute, October, 1990. [Dewar 90] R. B. K. Dewar. Shared Variables and Ada 9X Issues. Special Report SEI-90-SR-1, Software Engineering Institute, January, 1990. [DoD 88] Ada Board's Recommended Ada 9X Strategy Office of the Under Secretary of Defense for Acquisition, Washington, D.C., 1988. [DoD 89a] Ada 9X Project Plan Office of the Under Secretary of Defense for Acquisition, Washington, D.C., 1989. [DoD 89b] Common Ada Programming Support Environment (APSE) Interface Set (CAIS) (Revision A) edition, United States Department of Defense, 1989. MIL-STD-1838A. [DoD 90] Ada 9X Requirements Office of the Under Secretary of Defense for Acquisition, Washington, D.C., 1990. [DoD 91] Ada 9X Requirements Rationale Office of the Under Secretary of Defense for Acquisition, Washington, D.C., 1991. [Ellis 90] M. A. Ellis and B. Stroustrup. The Annotated C++ Reference Manual. Addison-Wesley, Reading, MA, 1990. [Elrad 88] T. Elrad. Comprehensive Scheduling Controls for Ada Tasking. In Proceedings of the Second International Workshop on Real-Time Ada Issues, pages 12-19. Ada UK and United States Air Force Office of Scientific Research, Association for Computing Machinery, New York, NY, June 1-3, 1988. Appeared in Ada Letters, VIII(7). [Fowler 89] K. J. Fowler. A Study of Implementation-Dependent Pragmas and Attributes in Ada. Technical Report Ada 9X Project Report, Software Engineering Institute, November, 1989. [Goldberg 83] A. Goldberg and D. Robson. Smalltalk-80: The language and its implementation. Addison-Wesley, Reading, MA, 1983. [Guimaraes 91] N. Guimaraes. Building generic user interface tools: an experience with multiple inheritance. In Proceedings of the Conference on Object-Oriented Programming Systems, Languages, and Applications. 1991. [Hoare 73] C.A.R. Hoare. Towards a theory of parallel programming. In C.A.R. Hoare and R.H. Perrott (editor), Operating Systems Techniques. Academic Press, New York, 1973. [Hoare 74] C.A.R. Hoare. Monitors -- An Operating Systems Structuring Concept. Communications of the ACM 17(10):549-557, October, 1974. [Ingalls 86] D. H. H. Ingalls. A simple technique for handling multiple polymorphism. In OOPSLA'86 Conference Proceedings. 1986. [LaLonde 89] W. R. LaLonde. Designing families of data types using exemplars. Transactions on Programming Languages and Systems 11(2), April, 1989. [Meyer 88] B. Meyer. Object-Oriented Software Construction. Prentice Hall, Englewood Cliffs, NJ, 1988. [Schaffert 86] C. Schaffert, T. Cooper, B. Bullis, M. Kilian, and C. Wilpolt. An introduction to Trellis/Owl. In OOPSLA'86 Proceedings. Portland, OR, 1986. [Seidewitz 91] E. Seidewitz. Object-Oriented Programming Through Type Extension in Ada 9X. Ada Letters XI(2), March/April, 1991. [Wellings 83] A.J. Wellings and D. Keefe and G.M Tomlinson. A Problem with Ada and Resource Allocation. Ada Letters III(4):112-123, January/February, 1983. [Wellings 84] Wellings, A.J., Keeffe, D. and Tomlinson, G.M. A Problem with Ada and Resource Allocation. Ada Letters III(4):112-123, 1984. [Wirth 88] N. Wirth. Type Extensions. ACM Transactions on Programming Languages and Systems 10(2):204-214, April, 1988. Table of Contents 1. Introduction 2-1 1.1. Background 2-1 1.2. Approach 2-1 2. Overview of the Ada Language 2-1 2.1. Objects, Types, Classes and Operations 2-1 2.1.1. Objects and Their Types 2-1 2.1.2. Type Classes 2-2 2.1.3. Operations and Overloading 2-3 2.1.4. Class-Wide Types and Dispatching 2-3 2.1.5. Abstraction and Static Evaluation 2-3 2.2. Statements, Expressions and Elaboration 2-4 2.2.1. Declarative Parts 2-4 2.2.2. Assignments and Control Structures 2-4 2.2.3. Expressions 2-4 2.3. System Construction 2-4 2.3.1. Program Units 2-4 2.3.2. Private Types and Information Hiding 2-5 2.3.3. Object-Oriented Programming 2-5 2.3.4. Generic Units 2-5 2.3.5. Separate Compilation 2-5 2.3.6. Library Units 2-5 2.3.7. Program Composition 2-6 2.3.8. Interfacing to Other Languages 2-6 2.4. Multitasking 2-6 2.4.1. Tasks 2-6 2.4.2. Communication and Synchronization 2-6 2.4.2.1. Task Creation, Activation and Termination 2-6 2.4.2.2. Protected Records 2-6 2.4.2.3. Protected Operations 2-6 2.4.2.4. Entries, Queues, Rendezvous, and Barriers 2-6 2.4.2.5. Select Statements 2-6 2.4.3. Timing 2-7 2.4.4. Scheduling 2-7 2.5. Exception Handling 2-7 2.6. Input-Output 2-7 2.7. Low-Level Programming 2-7 2.7.1. Pragmas 2-7 2.7.2. Specifying Representations 3-1 2.7.3. Unprotected Shared Variables 3-1 2.7.4. Unchecked Programming 3-1 2.8. Application-Specific Facilities 3-1 2.9. Summary 3-1 3. Mapping the Ada 9X Requirements 3-1 3.1. International Users 3-1 3.2. Support for Programming Paradigms 3-1 3.2.1. Subprograms as Data 3-1 3.2.2. Subprogram Interoperability 3-2 3.2.3. Composition of Program Units 3-2 3.2.4. Storage Management 3-3 3.2.4.1. Finalization 3-4 3.2.5. Generics 3-5 3.2.6. Exceptions 3-5 3.2.7. Input/Output 3-6 3.3. Real-Time Requirements 3-6 3.3.1. Time 3-6 3.3.2. Task Scheduling and Real-Time Paradigms 3-6 3.3.2.1. Example of Conditional Critical Regions and Shared 3-7 Data 3.3.2.2. Task IDs, Dynamic Priorities, and Mode Change 3-7 3.3.3. Asynchronous Control of Execution 3-8 3.3.4. Asynchronous Communication 3-9 3.4. System Programming 3-10 3.4.1. Unsigned Integer Operations 3-10 3.4.2. Data Interoperability 3-10 3.4.3. Interrupts 3-10 3.4.4. Dynamic References 3-11 3.5. Parallel Processing 3-11 3.5.1. Shared Memory 3-11 3.5.2. Massively Parallel Architectures 3-12 3.5.3. Vector Architectures 3-12 3.5.4. Configuration of Parallel Programs 3-12 3.6. Distributed Processing 3-12 3.7. Safety-Critical and Trusted Applications 3-13 3.8. Information Systems 4-1 3.8.1. Decimal Based Types 4-1 3.8.2. Common Functions 4-1 3.9. Scientific and Mathematical Applications 4-1 3.10. General Requirements 4-1 3.10.1. Efficiency, Simplicity, and Consistency 4-1 3.11. Summary 4-1 4. In-Depth Analyses of Major Enhancements 4-1 4.1. Introduction 4-2 4.2. Object-Oriented Programming 4-2 4.2.1. Purpose and Requirements Satisfied 4-2 4.2.2. Brief Description 4-2 4.2.3. Benefits and Intended Use 4-3 4.2.3.1. Variant Programming 4-4 4.2.3.2. Heterogeneous Data Structures 4-5 4.2.3.3. Multiple Implementations 4-5 4.2.3.4. Multiple Dispatch 4-5 4.2.3.5. Encapsulation and Extension 4-6 4.2.3.6. Class-Wide Programming and Generic Units 4-6 4.2.4. Relationship to Previous Work 4-6 4.2.4.1. Data Abstraction 4-6 4.2.4.2. Inheritance Mechanisms 4-6 4.2.4.3. Run-Time Dispatch 4-7 4.2.4.4. Multiple Inheritance 4-7 4.3. Child Library Units 4-8 4.3.1. Purpose and Requirements Satisfied 4-8 4.3.2. Brief Description 4-8 4.3.3. Benefits and Intended Use 4-9 4.3.3.1. Avoiding Recompilation 4-9 4.3.3.2. Using Child Packages to Create New Interfaces 4-9 4.3.3.3. Using Structured Libraries for Subsystems 4-9 4.3.3.4. Using Structured Libraries for Portability 4-9 4.3.3.5. Object Oriented and Incremental Programming 4-10 4.3.4. Relationship to Previous Work 4-10 4.3.4.1. Previous Solutions 4-10 4.3.4.2. Alternative Approaches 4-10 4.4. Data-Oriented Synchronization 4-10 4.4.1. Purpose and Requirements Satisfied 4-11 4.4.2. Brief Description 4-11 4.4.3. The Requeue and Selective Entry Call Facilities 4-11 4.4.3.1. The Purpose of the Requeue Statement 4-11 4.4.3.2. The Purpose of the Selective Entry Call Facility 4-12 4.4.4. Benefits and Intended Use 4-12 4.4.4.1. Indivisible Counter Increment 4-12 4.4.4.2. Persistent Signals 4-13 4.4.4.3. Broadcast 4-13 4.4.4.4. Queued Waiting 4-14 4.4.4.5. Priorities 4-14 4.4.4.6. Interrupts 4-14 4.4.4.7. Disk Manager 4-15 4.4.5. Relationship to Previous Work S-1 4.5. Summary S-1 S. Section-by-Section Rationale S-1 S.1. Ada 9X Introduction S-1 S.1.1. Scope of the Standard S-1 S.1.1.1. Extent of the Standard S-1 S.1.1.2. Conformity of an Implementation With the Standard S-1 S.1.2. Structure of the Standard S-1 S.1.2.1. Rationale S-1 S.1.3. Design Goals and Sources S-1 S.1.4. Language Summary S-1 S.1.5. Method of Description and Syntax Notation S-1 S.1.6. Classification of Errors S-1 S.2. Ada 9X Lexical Elements S-3 S.3. Ada 9X Declarations and Types S-4 S.3.1. Declarations S-4 S.3.2. Objects and Named Numbers S-4 S.3.3. Types and Subtypes S-4 S.3.3.1. Type Declarations S-4 S.3.3.2. Subtype Declarations S-4 S.3.3.3. Classification of Operations S-4 S.3.4. Derived Types S-4 S.3.4.1. Tagged Types and Type Extensions [new] S-5 S.3.4.2. Operations of Tagged Types [new] S-5 S.3.5. Scalar Types S-6 S.3.5.1. Enumeration Types S-6 S.3.5.2. Character Types S-6 S.3.5.3. Boolean Types S-6 S.3.5.4. Integer Types S-6 S.3.5.5. Operations of Discrete Types S-6 S.3.5.6. Real Types S-6 S.3.5.7. Floating Point Types S-6 S.3.5.8. Operations of Floating Point Types S-6 S.3.5.9. Fixed Point Types S-6 S.3.5.10. Operations of Fixed Point Types S-6 S.3.6. Composite Types [new] S-6 S.3.6.1. Discriminants [originally 3.7.1] S-6 S.3.6.2. Discriminant Constraints [originally 3.7.2] S-7 S.3.7. Array Types [originally 3.6] S-7 S.3.7.1. Index Constraints and Discrete Ranges [originally S-7 3.6.1] S.3.7.2. Operations of Array Types [originally 3.6.2] S-7 S.3.7.3. String Types [originally 3.6.3] S-7 S.3.8. Record Types [originally 3.7] S-7 S.3.8.1. Variant Parts [originally 3.7.3] S-7 S.3.8.2. Record Extension [new] S-7 S.3.8.3. Operations of Record Types [originally 3.7.4] S-8 S.3.9. Access Types [originally 3.8] S-8 S.3.9.1. Incomplete Type Declarations [originally 3.8.1] S-8 S.3.9.2. Operations of Access Types [originally 3.8.2] S-8 S.3.10. Declarative Parts [originally 3.9] S-9 S.4. Ada 9X Names and Expressions S-10 S.4.1. Names S-10 S.4.1.1. Indexed Components S-10 S.4.1.2. Slices S-10 S.4.1.3. Selected Components S-10 S.4.1.4. Attributes S-10 S.4.2. Literals S-10 S.4.3. Aggregates S-10 S.4.3.1. Record Aggregates S-10 S.4.3.2. Array Aggregates S-10 S.4.4. Expressions S-10 S.4.5. Operators and Expression Evaluation S-10 S.4.5.1. Logical Operators and Short-circuit Control Forms S-10 S.4.5.2. Relational Operators and Membership Tests S-10 S.4.5.3. Binary Adding Operators S-11 S.4.5.4. Unary Adding Operators S-11 S.4.5.5. Multiplying Operators S-11 S.4.5.6. Highest Precedence Operators S-11 S.4.6. Type Conversions S-11 S.4.7. Qualified Expressions S-11 S.4.8. Allocators S-11 S.4.9. Static Expressions and Static Subtypes S-11 S.4.9.1. Statically Matching Constraints and Subtypes [new] S-12 S.5. Ada 9X Statements S-13 S.6. Ada 9X Subprograms S-14 S.6.1. Subprogram Declarations S-14 S.6.2. Formal Parameter Modes S-14 S.6.3. Subprogram Bodies S-14 S.6.3.1. Conformance Rules S-14 S.6.3.2. Inline Expansion of Subprograms S-14 S.6.4. Subprogram Calls S-14 S.6.4.1. Parameter Associations S-14 S.6.4.2. Default Parameters S-14 S.6.5. Function Subprograms S-14 S.6.6. Parameter and Result Type Profile - Overloading of S-14 Subprograms S.6.7. Overloading of Operators S-14 S.7. Ada 9X Packages S-15 S.7.1. Package Structure S-15 S.7.2. Package Specifications and Declarations S-15 S.7.3. Package Bodies S-15 S.7.4. Private Type and Deferred Constant Declarations S-15 S.7.4.1. Private Types S-15 S.7.4.2. Private Extension [new] S-15 S.7.4.3. Operations of a Private Type [originally 7.4.2] S-15 S.7.4.4. Deferred Constants [originally 7.4.3] S-15 S.7.4.5. Limited Types [originally 7.4.4] S-15 S.7.5. Example of a Table Management Package S-15 S.7.6. Example of a Text Handling Package S-15 S.8. Ada 9X Visibility Rules S-16 S.8.1. Declarative Region S-16 S.8.2. Scope of Declarations S-16 S.8.3. Visibility S-16 S.8.4. Use Clauses S-16 S.8.5. Renaming Declarations S-16 S.8.6. The Package STANDARD S-16 S.8.7. The Context of Overload Resolution S-16 S.9. Ada 9X Tasks and Synchronization S-17 S.9.1. Task Specifications and Task Bodies S-17 S.9.2. Task Types and Task Objects S-17 S.9.3. Task Execution - Task Activation S-17 S.9.4. Task Dependence - Termination of Tasks S-17 S.9.5. Protected Record Specification and Protected Bodies [new] S-17 S.9.6. Protected Record Types and Protected Record Objects [new] S-17 S.9.7. Protected Operations, Entry Calls, and Accept Statements S-17 [originally 9.5] S.9.7.1. Requeue Statements [new] S-18 S.9.8. Delay Statements, Duration, and Time [originally 9.6] S-19 S.9.9. Select Statements [originally 9.7] S-19 S.9.9.1. Selective Accept [originally 9.7.1 ``Selective Wait''] S-20 S.9.9.2. Selective Entry Calls [consolidation of 9.7.2 and S-20 9.7.3] S.9.9.3. Asynchronous Transfer of Control [new] S-20 S.9.10. Priorities [originally 9.8] S-20 S.9.11. Task, Protected Record, and Entry Attributes [originally S-20 9.9] S.9.12. Abort of a Task; Abort of a Sequence of Statements S-20 [originally 9.10] S.9.13. Shared variables [originally 9.11] S-21 S.9.14. Example of Tasking and Synchronization [originally 9.12] S-21 S.10. Ada 9X Program Structure and Compilation Issues S-22 S.10.1. Compilation Units - Library Units S-22 S.10.1.1. Context Clauses - With Clauses S-22 S.10.1.2. Main Subprograms, Partitions, and Environment Tasks S-22 [new] S.10.1.3. Examples of Compilation Units [originally 10.1.2] S-22 S.10.2. Subunits of Compilation Units S-22 S.10.2.1. Examples of Subunits S-22 S.10.3. Order of Compilation S-22 S.10.4. The Program Library S-22 S.10.5. Elaboration of Library Units S-23 S.10.5.1. Library Unit Package Categorization [new] S-23 S.10.6. Program Optimization S-23 S.11. Ada 9X Exceptions S-24 S.11.1. Exception Declarations S-24 S.11.2. Exception Handlers S-24 S.11.3. Raise Statements [originally 11.3] S-24 S.11.4. Exception Handling S-24 S.11.4.1. Exceptions Raised During the Execution of Statements S-24 S.11.4.2. Exceptions Raised During the Elaboration of S-24 Declarations S.11.5. Exceptions Raised During Task Communication S-24 S.11.6. Exceptions and Optimization S-24 S.11.7. Suppressing Checks S-24 S.12. Ada 9X Generic Units S-25 S.12.1. Generic Declarations S-25 S.12.1.1. Generic Formal Objects S-25 S.12.1.2. Generic Formal Types S-25 S.12.1.3. Generic Formal Subprograms S-25 S.12.1.4. Generic Formal Packages [new] S-25 S.12.2. Generic Bodies S-25 S.12.3. Generic Instantiation S-26 S.12.3.1. Matching Rules for Formal Objects S-26 S.12.3.2. Matching Rules for Formal Private Types S-26 S.12.3.3. Matching Rules for Formal Scalar Types S-26 S.12.3.4. Matching Rules for Formal Array Types S-26 S.12.3.5. Matching Rules for Formal Access Types S-26 S.12.3.6. Matching Rules for Formal Derived Types [new] S-26 S.12.3.7. Matching Rules for Formal Subprograms [originally S-26 12.3.6] S.12.3.8. Matching Rules for Formal Packages [new] S-26 S.12.4. Example of a Generic Package S-26 S.13. Ada 9X Representation Clauses and Implementation-Dependent S-27 Features S.13.1. Representation Clauses S-27 S.13.2. Attribute Definition Clauses [originally Length Clauses] S-27 S.13.3. Enumeration Representation Clauses S-27 S.13.4. Record Representation Clauses S-27 S.13.5. Address Clauses S-27 S.13.5.1. Interrupts S-27 S.13.6. Change of Representation S-27 S.13.7. The Package SYSTEM S-27 S.13.7.1. System-Dependent Named Numbers S-27 S.13.7.2. Representation Attributes S-27 S.13.7.3. Representation Attributes of Real Types S-27 S.13.8. Machine Code Insertions S-27 S.13.9. Interface to Other Languages S-27 S.13.10. Unchecked Programming S-27 S.13.10.1. Unchecked Storage Deallocation S-27 S.13.10.2. Unchecked Type Conversions S-27 S.13.10.3. Unchecked Access Value Creation S-27 S.14. Ada 9X Input-Output U-1 S.14.1. External Files and File Objects U-1 S.14.2. Sequential and Direct Files U-1 S.14.2.1. File Management U-1 S.14.2.2. Sequential Input-Output U-1 S.14.2.3. Specification of the Package Sequential_IO U-1 S.14.2.4. Direct Input-Output U-1 S.14.2.5. Specification of the Package Direct_IO U-1 S.14.3. Text Input-Output U-1 S.14.3.1. File Management U-1 S.14.3.2. Default Input and Output Files U-1 S.14.3.3. Specification of Line and Page Lengths U-1 S.14.3.4. Operations on Columns, Lines, and Pages U-1 S.14.3.5. Get and Put Procedures U-1 S.14.3.6. Input-Output of Characters and Strings U-1 S.14.3.7. Input-Output for Integer Types U-1 S.14.3.8. Input-Output for Real Types U-1 S.14.3.9. Input-Output for Enumeration Types U-1 S.14.3.10. Specification of the Package Text_IO U-1 S.14.4. Exceptions in Input-Output U-1 S.14.5. Specification of the Package IO_Exceptions U-1 S.14.6. Low Level Input-Output U-1 S.14.7. Stream Input-Output [new] U-1 S.14.7.1. T'READ and T'WRITE [new] U-1 S.14.7.2. Predefined T'WRITE and T'READ [new] U-1 S.14.7.3. Validation on T'READ [new] U-1 S.14.7.4. T'WRITE and T'READ for Tagged Class-Wide Types [new] U-1 S.14.7.5. The Package STREAM_SUPPORT [new] U-1 S.14.7.6. Heterogeneous Input-Output [new] U-1 S.14.8. Example of Input-Output U-1 U. Analysis of Ada 9X Upward Compatibility/Consistency U-1 U.1. Incompatibilities U-2 U.2. Summary of Incompatibilities R-1 U.3. Note on NUMERIC_ERROR R-1 References R-1 List of Figures Figure 2-1: Ada Class Hierarchy 2-2 Figure 2-2: A User-Defined Class Hierarchy 2-2 List of Tables Table 2-1: Summary of Communication and Synchronization 2-7