!topic LSN on Limited Function Return and User-defined Clone !key LSN-1056 on Limited Function Return and User-defined Clone !reference MS-7.4.5;4.6 !reference LSN-1043 !from Bob Duff $Date: 92/11/05 13:32:36 $ $Revision: 1.2 $ !discussion LSN-1043 proposed a semantics for function return of inherently limited types that some have found unpalatable. For example, some people are uncomfortable with the idea of "deferred finalization", while others are concerned about the implementation of lazy stack cutting. On the other hand, we can't avoid the fact that for some kinds of types (e.g. finalizable types), a bit-wise copy simply won't work. Disallowing function return altogether for these kinds of types would be intolerable. In this LSN, we propose instead a fix-up operation, called Clone, that happens after the bit-wise copy. The user can define Clone so that it does whatever is necessary to perform a correct copy -- allocation of storage, incrementing of reference counts, or whatever. We refer to the bit-wise copy as the "physical copy". The Clone operation is defined to happen whenever a physical copy is performed -- function return, assignment, explicit initialization, etc. Thus, the user-defined assignment that some have been hoping for is part of this proposal. The Clone always happens after the physical copy. This proposal has the advantage of removing one of the three categories of limited types. LSN-1043 proposed "limited", "inherently limited", and "inherently limited uncopyable". In this LSN, however, inherently limited and uncopyable are the same thing. If something is inherently limited, it is a run-time error to attempt to copy it (by returning a local object from a function, for example). We still think we need "limited" and "inherently limited" to be different, for upward compatibility. CONTROLLED TYPES: Change the name of type Controlled to Limited_Controlled. Add a new non-limited type called Controlled. Thus, we have: type Controlled is tagged private; procedure Initialize(Object : in out Controlled); procedure Finalize(Object : in out Controlled) is <>; procedure Clone(Object : in out Controlled) is <>; type Limited_Controlled is tagged limited private; procedure Initialize(Object : in out Controlled); procedure Finalize(Object : in out Controlled) is <>; A descendant of Controlled or Limited_Controlled is called a "controlled type". We say "limited controlled type" or "non-limited controlled type" when the difference matters. The basic idea is that after a value is physically copied into an object, the Clone operation is automatically applied to that object. Any subcomponents of the object are also Clone-d. The Clone operations are always performed in bottom-up order -- the components first, then the whole object. Initialize and Finalize behave as explained elsewhere. In particular, Initialize is done bottom-up (first components, then the whole object). Finalize is done top-down (first the whole object, then the components). PARAMETER PASSING: All inherently limited types are passed by reference. All controlled types are passed by reference. FUNCTION RETURN: The semantics of function return is: For an inherently limited type (recall that objects and values go together for these types, so it makes sense to talk about the "result object"): If the result object is local, raise Program_Error. If the result object is global, return it by reference. For a type that is not inherently limited: Create a new object (the "result temp"). Physically copy the function result into the result temp. Perform the Clone operation on any parts of the result temp that are controlled. (After this, the function will be left, which will cause the normal finalization of locals, including, perhaps the object from which the result originated.) As stated above, returning a local inherently limited object will raise Program_Error. This check is not hard to perform in most implementations -- if the stack is a contiguous block of memory (the usual implementation), then the code simply tests whether the address of the result is between the old and new stack pointers. For a secondary stack model, it might be necessary to check both stacks. For a model that allocates stacks in chunks, a more complicated check is necessary. The result temp created in the non-inherently-limited case is finalized at some point after the caller is finished with it -- no later than the declaration or statement containing the function call. Note that Limited_Controlled is inherently limited; Controlled is not. ASSIGNMENT STATEMENT: The semantics of the assignment statement are: Evaluate the left-hand-side name and the right-hand-side expression, and perform constraint checks, all as in Ada 83. Finalize all controlled parts of the left-hand side. Physically copy the right-hand side into the left-hand side. Perform the Clone operation on any parts of the left-hand side object that are controlled. The implementation is allowed to go through an intermediate temp object, doing Clone-s and Finalize-s as necessary. It must use the intermediate temp object approach if any parts of the left- and right-hand sides overlap, as in "X.all := Y.all;" (where X and Y point to the same object) or "S(1..10) := S(2..11);". INITIALIZATION: Suppose we have: X: T; -- default initialization Y: T := F(...); -- explicit initialization The semantics of the default initialization of X are: Create the object, as in Ada 83. Perform default initialization, as in Ada 83. Perform the Initialize operation on any controlled parts. The semantics of the explicit initialization of Y are: Evaluate the initialization expression, create the object, and perform constraint checks, all as in Ada 83. Physically copy the value of the expression into the new object. Perform the Clone operation on any parts of the object that are controlled. OTHER COPY OPERATIONS: The other operations that involve copying are: - array catenation (the "&" operator) - default initialization of record components - passing of a generic formal parameter of mode 'in', either explicitly or by default. - an extension aggregate (Note that aggregate formation is not listed above -- aggregates are illegal for controlled types, since they are extensions of a private root type.) In each of the above operations, a new object (or group of objects) is being created. Thus, the rules can follow those defined above under INITIALIZATION. For example, for array catenation, each component of the result is Clone-d. For an extension aggregate, the parent part is Clone-d, and each controlled extension component is Clone-d; the extension aggregate as a whole is not Clone-d, since it hasn't been physically copied. The Clone operations always follow the same pattern -- first make the physical copy, then do Clone. The assignment statement is the only operation involving copying that needs to first Finalize the target object; in all the other cases, the target object is uninitialized. Note that we can't disallow any of the above operations without causing generic contract model problems, because non-limited controlled types can be passed to a generic formal (non-limited) private type. ABORT AND EXCEPTIONS: Abort is deferred during the groups of Finalize/Clone operations mentioned above. Abort is also deferred during each Initialize operation. This helps ensure that the operations do not happen too few or too many times. For example, for the assignment statement, if there are any controlled parts, abort is deferred from the first Finalize through the last Clone. The expression evaluations should happen outside the abort-deferred region. Similarly, if an exception occurs during a Finalize or Clone, other pending Finalize or Clone operations are done anyway. When they are all done, Program_Error is raised. This is just like in previous proposals for finalization. EFFICIENCY: For efficiency, we give special permission to the implementation to remove groups of Clone, Finalize, Physical-Copy operations that are in some sense redundant. The exact rules are not shown here. This requires the programmer to take care that these operations work properly under these terms. For example, if Finalize deallocates memory, and Clone allocates memory, this will be true. Storage_Error might be avoided in certain situations where it would otherwise be raised, but Storage_Error is unpredictable anyway. Similarly, if the Clone and Finalize operations keep reference counts, then (barring overflow) the reference counts will be correct, even if the compiler decides to remove certain groups of operations. For example, the statement "X.all := Y.all;" doesn't need to do anything if X = Y. Here's another example: X, Y: T := ...; function F return T is begin return X; end T; Y := F; The assignment statement "Y := F;" involves several physical copies, clones, and finalizations. However, if function F is inlined, the compiler might notice that most are redundant. It might generate code identical to that of "Y := X;". FINALIZATION OF TEMPORARY OBJECTS: The above assumes that temporary objects are created at certain points. These temps are finalized after they are no longer needed, and before the end of the current declaration or statement. However, the temp and its finalization are avoided if the compiler takes advantage of the permission to avoid redundant operations. For example: X := (A, Controlled_Object); For the assignment statement, the normal semantics would be to build the aggregate in a temporary object, Clone the controlled component, finalize the left-hand side, physically copy the aggregate into the X, and then Clone X. Alternatively, (assuming Controlled_Object is not part of X), the compiler is allowed to finalize X, build the aggregate in X, and Clone the controlled component of X. INTERACTIONS WITH VOLATILE AND ATOMIC: One issue that has been open for some time is the handling of volatile and atomic parameters. For example, suppose a composite object is declared Volatile (via the pragma, of course). If it is passed as a parameter, is the parameter still volatile? The subprogram doesn't know that the actual is volatile. If the parameter is passed by copy, there's no problem -- just the copy in and copy out operations, which generally happen at the call site, need to know about the volatility. But what if it's passed by reference? Furthermore, for a volatile object, the programmer needs to know when reads and writes are happening -- the maybe-by-copy, maybe-by-reference semantics doesn't work very well for such low-level programming. Given the above proposal, we add the following rules about atomic and volatile: Pragma Atomic is only allowed for an elementary object (either declared by an object_declaration, or by a component_declaration). If the object is a component, the containing type must be inherently limited. Pragma Volatile is allowed for objects as for Atomic. In addition, pragma Volatile is allowed on the declaration of an inherently limited type. (See below for a possible relaxation of this restriction to inherently limited types.) Thus, when passing an atomic or volatile object, the programmer has full knowledge of the parameter passing mechanism. If the parameter is elementary, it is passed by copy; the called subprogram works with a separate object. If the parameter is not elementary, the above rules ensure that it is inherently limited, and thus passed and returned by reference. In that case, the compiler can know that a particular component of the parameter is atomic or volatile, or that the parameter as a whole is volatile. Task and protected types are always volatile -- no need for pragma Volatile on them (although it is presumably legal). INTERACTIONS WITH ALIASED: We are considering requiring that composite objects with aliased components be passed by reference. We might do that by making aliased components cause the containing type to become inherently limited, or we might disallow aliased components of non-inherently limited types. Or, we might simply say that they are passed by reference, without requiring inherent limitedness. If we choose the latter approach, we might consider choosing a similar approach for the volatile case: Allow pragma Volatile on any declaration of a type that is not required to be passed by copy. Once marked volatile, all parameters of the type (or with a subcomponent of the type) must be passed by reference. SUMMARY: This LSN proposes what we believe to be a semantically simpler and more powerful approach to limited types than proposed by LSN-1043. Function return of finalizable types is possible. There is no need for deferred finalization or lazy stack cutting. User-defined assignment and initialization are supported.