AI22-0152-1
!standard 3.2.1(3) 26-07-15 AI22-0152-1/10
!standard 3.2.2(4)
!class Amendment 26-02-05
!status work item 26-02-05
!status received 25-11-07
!assigned author Tucker Taft
!submitter Tucker Taft
!priority Medium
!difficulty Hard
!subject Optional objects
A subtype may be specified to include a special value, designated by the "null" literal, which can represent an otherwise uninitialized object, or the absence of a value that is considered "optional". An object of such a subtype can be tested for having the null value by comparing it with "null" using an equality operator. Constraint_Error is raised if any other predefined operator is applied to a null value.
The notion of an "optional" or "maybe" object is quite common in many abstractions. In Ada this can be represented by a discriminated type with a boolean discriminant, whose variant part has a value only in the "True" variant. However, there is no standard discriminated type for this purpose, and furthermore, to introduce an optional object such as this implies changes to almost all uses of the object. Alternative approaches are to identify a special value of the type, such as zero or -1 for an integer, or some extra enumeration literal such as "No_Value". To accommodate such a special value, the programmer will typically need to define a named subtype for the valid values, and choose carefully between the "full" subtype and the "valid" subtype. Note that in some cases this existing approach will be more understandable, so programmers can still choose to use this approach when appropriate, but it depends on the original definer of the type anticipating the need to declare optional instances of the type.
By contrast, all access types in Ada have a special "null" value which can serve the purpose of an optional value, or be used to detect an unintentionally uninitialized value. Access subtypes can specify that they exclude this null value by specifying "not null", and this indicator can be used directly on formal parameters, results, objects, components, etc.
Because access types have this built-in notion of optionality via "null", they are sometimes used to represent components that are optional. However a direct ability to have optional components might be a significantly better solution, because an optional component would not suffer from many of the complexities of explicit pointers, such as explicit storage management, concerns about dangling references or storage leaks, and an extra level of indirection (".all") required in some but not all contexts.
We should consider adding a general notion of optionality into Ada, including to support the notion of recursive types without explicit use of access types[a][b][c][d], by making the recursive components optional[e].
The general notion of optionality can be incorporated into Ada by having a standardized and flexible notion of an optional value for all types, but with the default being "not null", presuming there is a simple syntax for specifying that a given object/subtype allows null. Furthermore, by generalizing this notion of optionality to components of a composite type, we open up the ability to define recursive data types such as trees or linked lists, so long as the nested component is declared optional. This makes the notion of optionality even more flexible and powerful.
We propose to allow any sort of type to have subtypes that allow a null value, by prepending the reserved words "null or" to a subtype_mark, analogous to using "not null" as a prefix to specify that a subtype disallows null. Given a named subtype that allows null, applying the "not null" prefix would produce a subtype that excludes null. Given a named subtype that disallows null, applying the "null or" prefix would produce a subtype that allows null. Access types would by default allow null in their first subtype, as they do now, while for any other kind of type, the first subtype would by default disallow null, as in Ada currently.
The model is that all types include a null value, but a given subtype might or might not allow the null value, and whether the first subtype includes this value depends on the kind of type. Here are examples illustrating the default for first subtypes, and the ability to include or exclude null when declaring a new subtype:
type X is access T; -- allows null
subtype N is not null X; -- disallows null
subtype M is null or N; -- allows null
type J is range 1..100; -- disallows null
subtype K is null or J; -- allows null
subtype L is not null K; -- disallows null
The "or" in "null or S" is effectively constructing a new subtype as a union of two subtypes of the same type, "null" which is representing a subtype containing only the value null, and subtype S which represents some other subset of the values of the type. So it should be seen as a constructor of a new subtype, rather than as a constraint on an existing subtype.
Any subtype that allows null has a default initial value of null, even if it is a subtype of a scalar type with a Default_Value specified (see the !discussion for more about this).
The predefined equality operator and the membership operation could be used to check for the presence of a null value, as for an access value:
A : K; -- defaults to null
...
if A /= null then
A := A + 1;
end if;
For limited types, which do not have a predefined equality operator, the membership operation would be the way to test for null. All predefined operators other than the equality operators would raise Constraint_Error if any operand is null.
Note that in some languages the possibility of an exception due to trying to use an optional value that is in fact not present is prevented by requiring an explicit "unwrapping" of an optional value. Such an unwrapping can only be performed as part of a case/switch/match statement that ensures the optional value is in fact present. We do not propose that we impose such additional complexity, given that Ada currently allows access types to be dereferenced without such an explicit unwrapping, and more generally, Ada has numerous operations that use run-time checking rather than forcing defensive coding. On the other hand, it would be reasonable to provide a way for a user or project to self-impose a restriction to require defensive coding so all such checks could be performed at compile-time. In a separate AI (AI22-0165-1), we have proposed a way to specify that static checks are required for specified language-defined checks. This will require that the programmer perform defensive coding to ensure that such run-time failures could not occur.
Overload resolution for null would generally allow any "single" (specific) type (currently it is any single access type), as well as class-wide and universal types. To minimize the creation of new ambiguities with this rule, we propose a rule that when the literal null is passed as an actual parameter in a call, there is a Name Resolution rule that the formal parameter subtype shall either be a subtype that allows null or be an access subtype. This is a new sort of Name Resolution rule that depends on a subtype, and might not be necessary if we believe the number of incompatibilities caused by this new potential ambiguity is very small. See below for further discussion of overload resolution in the Implementation Notes section of the !discussion.
Static matching and static compatibility are updated for this new kind of subtype. In particular, static matching needs to require that both or neither allow null. Static compatibility (which is not a symmetric relationship) would require that if S2 (the target) does not allow null, then S1 (the source) also does not allow null (similar to null exclusions).
The original proposal suggested using a suffix of "or null" rather than a prefix of "null or". However, the general desire to allow first subtypes to be declared as including the null value implies we need a uniform syntax for indicating optionality in all type declaration syntaxes. It was proposed that the prefix of "null or" could immediately follow the "is" in essentially any type declaration, and not be buried at the end of a large type definition. Given this, we suggest that we use a "null or" prefix uniformly for type and subtype declarations.
We propose that a generic formal type may specify "null or" as well, and the formal type and the actual type must match in whether they allow null (unless the formal is null indeterminate – see below).
Private types and formal private types are interesting; we need to remember that currently they are already allowed to correspond to an access type, whether or not the actual/full subtype is null-excluding. Our proposal is that, for both normal, untagged private types and formal untagged private types, unless there is an explicit "not null" (or "null or") in the private type declaration, the private type is considered null indeterminate. You cannot specify a not null subtype of a null indeterminate type, because the operations of the private type might assume they can freely use and return null. Note that we do allow declaring a subtype that includes null, but the user must still assume that the operations of the private type might not allow null, so objects with a null value that are not created by an operation of the private type should not be passed back to such operations to avoid a possible Constraint_Error. Note that a membership test can be used to check whether a given value is "in" the private type, even if it doesn't declare whether or not it includes null among its values.
This default for an untagged private type of being null indeterminate is easily overcome by including an explicit "not null" (or "null or") in the (formal) private type declaration. The actual/full subtype must then match that specification, and in particular it must not be null indeterminate. If neither null-controlling prefix is present, the actual/full subtype might or might not allow null. The code that only sees the partial view does not know whether "internally" it might be making use of a null value, which means that code must not presume whether or not the operations of the type allow null. This approach of making a (formal) private type, by default, null indeterminate, is important, because the implementor of the full type might be using null for some other purpose, such as representing a closed file descriptor.
Note that we could go further and disallow declaring a null-allowing subtype based on a null indeterminate type. That would be safer in some sense, but it would also mean that a user could not declare an optional instance of a private type that didn't give an explicit null inclusion or null exclusion in its original declaration, and our original goal was that a user could declare an optional instance of any type.
Roughly the same logic applies to incomplete views of types. We consider them null indeterminate, but allow a null inclusion. However, we allow a null inclusion only if the type is completed in the same list of declarations, to avoid any issues with representation choice for the null value.
Given this proposed notion of optionality, we can now allow the name of a composite type to be used as the type of one of its own components so long as the component is of a subtype that allows null. This allows the definition of recursive data structures without explicit pointers:
type Tree is record
Data : Element;
Left, Right : null or Tree; -- Left and Right default to null
end record;
Note that this will be another case where a type name used within its own declaration is not a current instance – see 8.6(17.1-17.5). The ability to define recursive data structures without pointers is one of the great benefits of optional objects. The subtrees in a type like Tree by definition have only one reference, meaning that any storage management is simplified because, since there are no explicit pointers, there is no need for reference counts, and no worries about dangling references or storage leaks. Remember that an optional component is still treated like a direct component of the enclosing object, and assigning to or from such an optional component is done by copy[f][g][h][i][j], not by reference. Move and Swap operations are proposed in a separate AI (AI22-0164-1) to reduce the amount of copying in certain cases.
Here is an example of building up and then chopping back down a Tree:
X : null or Tree; -- default initialized to null
...
X := (Data => E, others => <>);
-- Initialize the tree to have one node
X.Left := (Data => F, Left => <>, Right => (Data => G, others =>
<>));
-- Fill in the Left component of the tree.
...
X.Left.Right := null;
-- Set a subcomponent to null, thereby shrinking the tree.
...
X := null; -- Set the whole tree back to empty
Replace "(a) null string" with "(an) empty string" in:
2.6 String Literals, 4.2 Literals, A.3.2 The Package Characters.Handling, A.4.2 The Package Strings.Maps, A.4.3 Fixed-Length String Handling, A.4.4 Bounded-Length String Handling, A.4.5 Unbounded-Length String Handling, A.8.2 File Management, A.15 The Package Command_Line, A.16 The Package Directories, A.16.1 The Package Directories.Hierarchical_File_Names, B.3 Interfacing with C and C++, F.3.3 The Package Text_IO.Editing
Replace "(a) null array" with "(an) empty array" in:
3.6.1 Index Constraints and Discrete Ranges,
3.6.2 Operations of Array Types,
3.9 Tagged Types and Type Extensions,
4.5.2 Relational Operators and Membership Tests,
4.5.3 Binary Adding Operators,
4.10 Image Attributes,
5.5.2 Generalized Loop Iteration,
B.4 Interfacing with COBOL
Replace "(a) null range" with "(an) empty range" in:
3.5 Scalar Types
3.6 Array Types
3.6.1 Index Constraints and Discrete Ranges
3.6.2 Operations of Array Types
4.1.2 Slices
4.3.3 Array Aggregates
5.5 Loop Statements
A.5.2 Random Number Generation
K.2 Language-Defined Attributes
Modify 3.2(7/2):
{In addition to the normal values of a type, every type includes a null value, which can be used to represent the absence of a normal value. }The set of {(nonnull) }possible values for an object of a given type can be subjected to a condition that is called a constraint (the case of [a null]{an empty} constraint [o][p]that specifies no restriction is also included)[Redundant:; the rules for which values satisfy a given kind of constraint are given in 3.5 for range_constraints, 3.6.1 for index_constraints, and 3.7.1 for discriminant_constraints]. The set of possible values for an object of [an access]{a} type can also be subjected to a condition that excludes the null value (see [3.10]{3.2.2}).
Modify 3.2(7.a/5):
Ramification: “[Null]{Empty} constraint” includes the cases of no explicit constraint, as well as unknown discriminants and unconstrained array type declarations (which are explicit ways to declare no constraint).
Modify 3.2(9.a-9.b):
Discussion: In an earlier version of Ada 9X, "constrained" meant "has a [nonnull]{nonempty} constraint." However, we changed to this definition since we kept having to special case composite non-array/nondiscriminated types. It also corresponds better to the (now obsolescent) attribute 'Constrained.
For scalar types, “constrained” means “has a [nonnull]{nonempty} constraint”. For composite types, in implementation terms, “constrained” means that the size of all objects of the subtype is the same, assuming a typical implementation model.
Replace 3.2.1(4/2) with:
type_definition ::=
[null_inclusion] enumeration_type_definition | [null_inclusion] integer_type_definition
| [null_inclusion] real_type_definition | [null_inclusion] array_type_definition
| [null_inclusion] record_type_definition | access_type_definition
| derived_type_definition | interface_type_definition
Modify 3.2.1(5):
A given type shall not have a subcomponent whose type is the given type itself{, unless the subcomponent is a part of some subcomponent of the type that includes the null value (see 3.2.2)}.
[Author's note: If we approve AI22-0148-1 on Bounded and Unbounded indefinite objects, this might change further.]
Replace 3.2.2(3/2) with:
subtype_indication ::= [null_control] subtype_mark [constraint]
null_control ::= null_exclusion | null_inclusion
null_exclusion ::= not null
null_inclusion ::= null or
Add after 3.2.2(8):
Static Semantics
A null_exclusion in a construct specifies that the null value does not belong to the subtype defined by the construct, that is, the subtype excludes null. Similarly, a null_inclusion in a construct specifies that the null value belongs to the subtype defined by the construct, that is, the subtype includes null. For a subtype_indication without a null_control, the subtype denoted by the subtype_indication includes or excludes null if and only if the subtype denoted by the subtype_mark in the subtype_indication includes or excludes null.
In the absence of a null_inclusion, the first subtype of a type defined by a full type definition other than for an access type or a derived type excludes null. [Redundant: The first subtype of an access type includes null in the absence of a null_exclusion (see 3.10). The first subtype of a derived type includes or excludes null if and only if the parent subtype includes or excludes null. In the absence of a null_control, an untagged private type or an untagged formal private type is null indeterminate, as is its first subtype (see 7.3 and 12.5.1). An incomplete view of a type is also null indeterminate, as is its first subtype (see 3.10.1).]
Legality Rules
In a subtype_indication, discriminant_specification, parameter_specification, parameter_and_result_profile, object_renaming_declaration, or formal_object_declaration, a null_exclusion shall be specified only if the subtype_mark denotes a subtype that includes null and is not of a null indeterminate type; a null_inclusion shall be specified only if the subtype_mark denotes a subtype that excludes null, or is null indeterminate.
AARM Reason: When a (private) type is null indeterminate, it is forbidden to define a subtype that excludes null, because that would break privacy of the implementation of the private type, since it might be using null for internal purposes, such as representing an empty container. On the other hand, it is safe to define a subtype that includes null for a null indeterminate private type. However, the full subtype might already include null, which would imply that a null value could have a special meaning to the implementation of the private type. A membership test can be used to check whether a given value of a null indeterminate private type is meaningful by testing whether it is a member of the first subtype of the private type. Note that this is not an issue for tagged private types, as their full type could not be an access type.
Modify 3.2.2(12):
The condition imposed by a constraint is the condition obtained after elaboration of the constraint. The rules defining compatibility are given for each form of constraint in the appropriate subclause. These rules are such that if a constraint is compatible with a subtype, then the condition imposed by the constraint cannot contradict any condition already imposed by the subtype on its values. {A null_exclusion is compatible with a subtype that includes null and is not of a null indeterminate type. A null_inclusion is compatible with a subtype that excludes null or is null indeterminate. }The exception Constraint_Error is raised if any check of compatibility fails.{
A value satisfies any exclusion of the null value if it is not the null value of its type.}
Add after 3.2.3(8/6):
Dynamic Semantics
For a predefined operation of a type other than an equality operator (see 4.5.1), membership test (see 4.5.2), assignment operation (see 5.2), or certain attributes (see 4.1.4), a check is made that no operand has the null value of the type. Constraint_Error is raised if this check fails.
Modify 3.2.4(29.2/4):
To determine whether a value satisfies the predicates of a subtype S, {if the value is not null, }the following tests are performed in the following order, until one of the tests fails, in which case the predicates are not satisfied and no further tests are performed, or all of the tests succeed{ (or the value is null)}, in which case the predicates are satisfied:
Modify 3.3.1(11):
Modify 3.3.1(11.1/5):
Modify 3.3.1(13/5):
Modify 3.3.1(21/5):
[Redundant: There is no implicit initial value defined for a scalar subtype{ that excludes null} unless the Default_Value aspect has been specified for the type.] In the absence of an explicit initialization or the specification of the Default_Value aspect, a newly created scalar object{ whose subtype excludes null} can have a value that does not belong to its subtype (see 13.9.1 and H.1).
Modify 3.4(6.1/2):
The first subtype of the derived type excludes [null (see 3.10) if and only if the parent subtype excludes null] {or includes null, or is null indeterminate (see 3.2.2), according to the parent subtype; the derived type is null indeterminate if and only if the parent type is null indeterminate}.
Modify 3.4.1(6/5):
Universal types
Universal types are defined for [Redundant: (and belong to)] the integer, real, fixed point, and access classes, and are referred to in this document as respectively, universal_integer, universal_real, universal_fixed, and universal_access. These are analogous to class-wide types for these language-defined elementary classes. As with class-wide types, if a formal parameter is of a universal type, then an actual parameter of any type in the corresponding class is acceptable. In addition, a value of a universal type (including an integer or real numeric_literal[, or the literal null]) is “universal” in that it is acceptable where some particular type in the class is expected (see 8.6).
Modify 3.5(9):
The elaboration of a range_constraint consists of the evaluation of the range. The evaluation of a range determines a lower bound and an upper bound. If simple_expressions are given to specify bounds, the evaluation of the range evaluates these simple_expressions in an arbitrary order, and converts them to the type of the range{; if either bound is the null value, Constraint_Error is raised}. If a range_attribute_reference is given, the evaluation of the range consists of the evaluation of the range_attribute_reference.
Modify 3.5(39.3/2):
This function returns a value given an image of the value as a Wide_Wide_String, ignoring any leading or trailing spaces.{ If the image matches the string "null" ignoring case, the function returns the null value of the type. For any other image, the result depends on the nature of the subtype S, as described below.}
Modify 3.5(42):
This function returns a value given an image of the value as a Wide_String, ignoring any leading or trailing spaces.{ If the image matches the string "null" ignoring case, the function returns the null value of the type. For any other image, the result depends on the nature of the subtype S, as described below.}
Modify 3.5(54):
This function returns a value given an image of the value as a String, ignoring any leading or trailing spaces.{ If the image matches the string "null" ignoring case, the function returns the null value of the type. For any other image, the result depends on the nature of the subtype S, as described below.}
Modify 3.6(9):
Each index_subtype_definition or discrete_subtype_definition in an array_type_definition defines an index subtype; its type (the index type) shall be discrete.{ The subtype_mark of an index_subtype_definition shall denote a subtype that excludes null; a discrete_subtype_definition shall define a subtype that excludes null.}
Modify 3.6(22.1/3):
For an array type with a scalar component {sub}type{ that excludes null}, the following language-defined representation aspect may be specified with an aspect_specification (see 13.1.1):
Replace "[null_exclusion]" with "[null_control]" in:
discriminant_specification 3.7, parameter_and_result_profile 6.1, parameter_specification 6.1[q][r]
Modify 3.9.2(10/2):
In the declaration of a dispatching operation of a tagged type, everywhere a subtype of the tagged type appears as a subtype of the profile (see 6.1), it shall statically match the first subtype of the tagged type{ unless the first subtype includes null, in which case it shall match the result after excluding null from the first subtype}. If the dispatching operation overrides an inherited subprogram, it shall be subtype conformant with the inherited subprogram. The convention of an inherited dispatching operation is the convention of the corresponding primitive operation of the parent or progenitor type. The default convention of a dispatching operation that overrides an inherited primitive operation is the convention of the inherited operation; if the operation overrides multiple inherited operations, then they shall all have the same convention. An explicitly declared dispatching operation shall not be of convention Intrinsic.
Delete 3.10(5.1/2):
null_exclusion ::= not null
Modify 3.10(13/2):
[For each access type, there is a null access value designating no entity at all, which can be obtained by (implicitly) converting the literal null to the access type. ][Redundant: The null value of an access type is the default initial value of the type{, and designates no entity at all}.] Nonnull values of an access-to-object type are obtained by evaluating an allocator[Redundant:, which returns an access value designating a newly created object (see 3.10.2)], or in the case of a general access-to-object type, evaluating an attribute_reference for the Access or Unchecked_Access attribute of an aliased view of an object. Nonnull values of an access-to-subprogram type are obtained by evaluating an attribute_reference for the Access attribute of a nonintrinsic subprogram.
Modify 3.10(13.1/2):
[A null_exclusion in a construct specifies that the null value does not belong to the access subtype defined by the construct, that is, the access subtype excludes null. In addition, the]{The} anonymous access subtype defined by the access_definition for a controlling access parameter (see 3.9.2) excludes null{ (see 3.2.2). For other access types, the first subtype includes null, in the absence of an explicit null_exclusion}.[ Finally, for a subtype_indication without a null_exclusion, the subtype denoted by the subtype_indication excludes null if and only if the subtype denoted by the subtype_mark in the subtype_indication excludes null.]
Delete 3.10(14.1/2):
If a subtype_indication, discriminant_specification, parameter_specification, parameter_and_result_profile, object_renaming_declaration, or formal_object_declaration has a null_exclusion, the subtype_mark in that construct shall denote an access subtype that does not exclude null.
Modify 3.10(15/2):
A composite_constraint is compatible with an unconstrained access subtype if it is compatible with the designated subtype. [A null_exclusion is compatible with any access subtype that does not exclude null. ]An access value satisfies a composite_constraint of an access subtype if it equals the null value of its type or if it designates an object whose value satisfies the constraint.[ An access value satisfies an exclusion of the null value if it does not equal the null value of its type.]
Modify 3.10.1(2.1/4):
An incomplete_type_declaration declares an incomplete view of a type and its first subtype; the first subtype is unconstrained if a discriminant_part appears.{ The incomplete view and its first subtype are null indeterminate (see 3.2.2).} If the incomplete_type_declaration includes the reserved word tagged, it declares a tagged incomplete view. If T denotes a tagged incomplete view, then T'Class denotes a tagged incomplete view. [Redundant: An incomplete view of a type is a limited view of the type (see 7.5).]
Add after 3.10.1(8.1/6):
[Authors's note: If AI22-0148-1 on Bounded and Unbounded Indefinite objects is approved, we can relax this restriction further. See that AI for further discussion.]
Modify 4.1.1(7):
For the evaluation of an indexed_component, the prefix and the expressions are evaluated in an arbitrary order. The value of each expression is converted to the corresponding index type.{ A check is made that the value of the prefix is not the null value of its type. If this check succeeds, a}[A] check is made that each index value belongs to the corresponding index range of the array or entry family denoted by the prefix. Constraint_Error is raised if [this check fails]{either of these checks fail}.
Modify 4.1.3(14):
The evaluation of a selected_component includes the evaluation of the prefix.{ If the prefix denotes a value or an object, a check is made that the value of the prefix is not the null value of its type. Constraint_Error is raised if this check fails.}
Modify 4.1.4(11/5):
The evaluation of a range_attribute_reference or an attribute_reference that is not a reduction_attribute_reference consists of the evaluation of the prefix.[Redundant: The evaluation of a reduction_attribute_reference is defined in 4.5.10.]{ If the prefix denotes a value or an object, and the prefix is part of a range_attribute_reference, or the attribute_designator identifies an operational attribute (see 13.3) other than Access, Unchecked_Access, Old, Valid, or an image attribute (see 4.10), a check is made that the value of the prefix is not the null value of its type. Constraint_Error is raised if this check fails.}
Replace 4.2(2/2) with:
The expected type for the literal null shall be a single type, or a universal or class-wide type; the literal resolves to its expected type.
[Author's note: When combined with the change to 6.4.1(3), this is intended to provide close compatibility with the existing language, while permitting comparisons of any object against null.]
Delete 4.2(8/5):
The literal null is of type universal_access.
Replace throughout 4.3.3 "(a) null_array_aggregate" with "(an) empty_array_aggregate".[s]
Insert before 4.5(10):
All predefined operators other than the equality operators (see 4.5.2) raise Constraint_Error if any operand is the null value.
Insert before 4.5.2(10):
For the predefined equality operators, if either operand is the null value, the operands are considered equal if and only if both operands are the null value. [Redundant: For any other relational operator, a check is made that neither operand is the null value. If this check fails, Constraint_Error is raised.] The rules given below are applied if neither operand is the null value.
Modify 4.5.2(28/3):
{An individual membership test where the value of the tested_simple_expression is the null value of the tested type, yields the result True if:
}An individual membership test {where the value of the tested_simple_expression is not the null value of the tested type, }yields the result True if:
Delete 4.5.2(30.2/4):
Modify 4.6(28):
For the evaluation of a type_conversion that is a value conversion, the operand is evaluated, and then the value of the operand is converted to a corresponding value of the target type, if any. If there is no value of the target type that corresponds to the operand value, Constraint_Error is raised[Redundant: ; this can only happen on conversion to a modular type, and only when the operand value is outside the base range of the modular type.] {If the value is not null, additional rules apply:}[Additional rules follow:]
Delete 4.6(49/2):
Modify 4.6(51/5):
After conversion of the value to the target type,{ if the value is null, a check is made that the target subtype includes null. If the value is not null, further checks are performed:} if the target subtype is constrained, a check is performed that the value satisfies this constraint[.]{;} [If the target subtype excludes null, then a check is made that the value is not null. If]if predicate checks are enabled for the target subtype (see 3.2.4), a check is performed that the value satisfies the predicates of the target subtype, unless the conversion is:
Add after 4.9(4):
Modify 4.9(28):
Modify 4.9(44.v/2):
We clarify that the first subtype of a scalar formal type has a nonstatic, [nonnull]{nonempty} constraint.
Modify 4.9.1(1.1/2):
Delete 4.9.1(1.a/5):
Discussion: A null constraint has nothing to do with null exclusions! Unconstrained array subtypes, subtypes with unknown discriminants, and subtypes with no explicit constraint have null constraints (see 3.2). This terminology became confusing when null exclusions were introduced in the 2007 Amendment.
Add after 4.10(7/6):
For a nonderived type or a nonnull type extension T, if the value is null, the default implementation for T'Put_Image will generate the image "NULL" in upper case. For nonnull values, the rules are given below.
Modify 4.10(15/5):
For an access type (named or anonymous), the image written out [depends on whether the value is null. If it is null, then the image is "NULL". Otherwise the image]{for a nonnull value} is a left parenthesis followed by "ACCESS", a space, and a sequence of graphic characters, other than space or right parenthesis, representing the location of the designated object, followed by a right parenthesis, as in "(ACCESS FF0012AC)".
Add after 6.3.1(16.2/3):
Add after 6.4.1(3):
If an actual parameter is the literal null, the formal parameter subtype shall be of an access type, or shall include null.
AARM Reason: This ensures compatibility with existing code where the presence of a null literal could be used to disambiguate between a call that expects an access type and one that does not.
Modify 6.4.1(18/3):
If the nominal subtype of a formal parameter with discriminants is constrained or indefinite, and the parameter is passed by reference, then the execution of the call is erroneous if the value of any discriminant of the actual is changed while the formal parameter exists (that is, before leaving the corresponding callable construct).{ Similarly, if an actual parameter is not null, and the corresponding formal parameter is passed by reference, then the execution of the call is erroneous if the value of the actual parameter is set to null while the formal parameter exists, unless the assignment is via the formal parameter.}[t][u][v][w][x][y][z][aa][ab][ac][ad]
Replace 7.3(2/3) with:
private_type_declaration ::=
type defining_identifier [discriminant_part] is [null_control] [[abstract] tagged] [limited] private
[aspect_specification];
Add after 7.3(13.1/6):
If an untagged private_type_declaration has a null_control, the full view shall be defined by a subtype that excludes or includes null, according to whether the null_control is a null_exclusion or a null_inclusion. If a tagged private_type_declaration has a null_control, it shall be a null_inclusion, and the full view shall be defined by a subtype that includes null; otherwise the full view shall be defined by a subtype that excludes null.
Add after 7.3(14):
If an untagged private_type_declaration has a null_control, the first subtype of the partial view excludes or includes null according to whether the null_control is a null_exclusion or a null_inclusion; otherwise, the partial view of the untagged type and its first subtype are null indeterminate (see 3.2.2). If a tagged private_type_declaration has a null_inclusion, the first subtype of the partial view includes null; otherwise it excludes null.
Modify 7.3.2(22/3):
The invariant check consists of the evaluation of each enabled invariant expression that applies to T, on each of the objects specified above{ whose value is not null; an invariant expression is defined to evaluate to True if the value of the checked object is null}. If any of these evaluate to False, Assertions.Assertion_Error is raised at the point of the object initialization, conversion, or call. If a given call requires more than one evaluation of an invariant expression, either for multiple objects of a single type or for multiple types with invariants, the evaluations are performed in an arbitrary order, and if one of them evaluates to False, it is not specified whether the others are evaluated. Any invariant check is performed prior to copying back any by-copy in out or out parameters. Invariant checks, any postcondition check, and any constraint or predicate checks associated with in out or out parameters are performed in an arbitrary order.
Modify 7.3.3(7/5):
If one or more default initial condition expressions apply to a [Redundant: (nonabstract)] type T, then a default initial condition check is performed after successful initialization of an object of type T by default{, other than to a null value} (see 3.3.1). In the case of a controlled type, the check is performed after the call to the type's Initialize procedure (see 7.6).
Modify 7.6(10/2):
During the elaboration or evaluation of a construct that causes an object to be initialized by default{ to a nonnull value}, for every controlled subcomponent of the object that is not assigned an initial value (as defined in 3.3.1), Initialize is called on that subcomponent. Similarly, if the object that is initialized by default{to a nonnull value is,} as a whole{,}[ is] controlled, Initialize is called on the object.
Replace 8.5.1(2/5) with:
object_renaming_declaration ::=
defining_identifier [: [null_control] subtype_mark] renames object_name
[aspect_specification];
| defining_identifier : access_definition renames object_name
[aspect_specification];
Modify 8.5.1(4.5/7-4.7/7):
For an object_renaming_declaration with a null_exclusion{ as its null_control,} or an access_definition that has a null_exclusion, the subtype of the object_name shall exclude null.{ For an object_renaming_declaration with a null_inclusion as its null_control, the subtype of the object_name shall include null.} In addition, if the object_renaming_declaration occurs within the body of a generic unit G or within the body of a generic unit declared within the declarative region of generic unit G, then:
Modify 8.6(17.2/6):
the subtype_mark of a subtype_indication[ of an object_declaration];
Replace 9.1(2/3) with:
task type defining_identifier [known_discriminant_part]
[aspect_specification] [is [null_inclusion]
[new interface_list with]
task_definition];
Replace 9.4(2/3) with:
protected_type_declaration ::=
protected type defining_identifier [known_discriminant_part]
[aspect_specification] is [null_inclusion]
[new interface_list with]
protected_definition;
Add after 11.5(15):
Null_Check
When evaluating a dereference (explicit or implicit), check that the value of the name is not null. When performing a predefined operation other than an equality operator, membership test, or assignment, check that no operand has the null value. When converting to a subtype that excludes null, check that the converted value is not null.
AARM Ramification: This is a superset of the checks covered by Access_Check. [Author's note: perhaps move definition of 11.5(11/2) Access_Check to Annex J.]
Replace 12.4(2/3) with:
formal_object_declaration ::=
defining_identifier_list : mode [null_control] subtype_mark [:= default_expression]
[aspect_specification];
| defining_identifier_list : mode access_definition [:= default_expression]
[aspect_specification];
Modify 12.4(8.3/5):
For a formal_object_declaration of mode in out with a null_exclusion{ as its null_control,} or an access_definition that has a null_exclusion, the subtype of the actual matching the formal_object_declaration shall exclude null.{ For a formal_object_declaration with a null_inclusion as its null_control, the subtype of the actual matching the formal_object_declaration shall include null.} In addition, if the actual matching the formal_object_declaration statically denotes the generic formal object of mode in out of another generic unit G, and the instantiation containing the actual occurs within the body of G or within the body of a generic unit declared within the declarative region of G, then the declaration of the formal object of G shall have a{ matching} null_exclusion{ or null_inclusion}.{ The nominal subtype of a formal_object_declaration with a subtype_mark but no null_control is null indeterminate (see 3.2.2) if the type of the subtype is an access type or a null indeterminate private type.} In addition to the places where Legality Rules normally apply (see 12.3), this rule applies also in the private part of an instance of a generic unit.
Replace 12.5(3/2) with:
formal_type_definition ::=
[null_control] formal_private_type_definition
| formal_derived_type_definition
| [null_inclusion] formal_discrete_type_definition
| [null_inclusion] formal_signed_integer_type_definition
| [null_inclusion] formal_modular_type_definition
| [null_inclusion] formal_floating_point_definition
| [null_inclusion] formal_ordinary_fixed_point_definition
| [null_inclusion] formal_decimal_fixed_point_definition
| [null_inclusion] formal_array_type_definition
| formal_access_type_definition
| formal_interface_type_definition
Add after 12.5(6/3):
In the absence of a null_control, an untagged formal private type is null indeterminate, as is its first subtype; otherwise the first subtype of the formal private type excludes or includes null according to the null_control. A formal interface type is null indeterminate, as is its first subtype. A formal derived type is null indeterminate if and only if its parent type is null indeterminate; its first subtype excludes or includes null, or is null indeterminate, according to the parent subtype. [Redundant: Whether the first subtype of an access subtype excludes or includes null is determined by the access_type_definition of the formal_access_type_definition. ]The first subtype for other formal types includes null if and only if there is a null_inclusion; otherwise it excludes null.
Add after 12.5(7/2):
If the formal subtype is null indeterminate, the actual subtype may exclude or include null, or be null indeterminate; otherwise, the actual subtype shall exclude or include null as determined by the formal subtype.
Modify 12.5.4(4/2):
If and only if the general_access_modifier constant applies to the formal, the actual shall be an access-to-constant type. If the general_access_modifier all applies to the formal, then the actual shall be a general access-to-variable type (see 3.10).[ If and only if the formal subtype excludes null, the actual subtype shall exclude null.]
Modify 13.13(17):
[Redundant: A type shall be completely defined before it is frozen{, as shall be the type of any component with an incomplete view as its subtype} (see 3.11.1 and 7.3).]
Among other uses, access values are often used for a reference to a potentially large, optional component. However, in many cases, this is the only access value that will ever refer to this pointed-to object, and so it is generally managed as though it "owns" the pointed-to object, meaning that when the enclosing object reaches its end of life, the pointed-to object is unconditionally deallocated. Supporting the notion of optional components directly in the language simplifies this situation, by having storage management, both allocation and deallocation, handled automatically, with no fear of storage leakage or dangling references. Furthermore, by specifying the component as an optional component rather than using an explicit access valued component, the programmer more clearly indicates their intent.
Both Rust and the SPARK subset of Ada explicitly support the notion of "pointer ownership", which does reduce the chance of dangling pointers or storage leaks. But here we are instead proposing what has been called "object ownership", which has many of the same benefits, but eliminates explicit pointers in favor of optional components. Large optional components are generally implemented behind the scenes using a level of indirection, but these pointers (or offsets) are all "owned" by the enclosing object, and the user never gets involved in allocating or deallocating the storage for these indirectly-referenced components. Optional objects can either have a null value (similar to a null pointer value), or have a non-null value of their type. The expectation is that the null value takes very little space, while the non-null value might be arbitrarily large. The fundamental improvement here is that programmers are no longer explicitly deallocating objects, and it is explicit deallocation that is the source of most memory unsafe operations in Ada. The US has recently directed government contractors to use "memory safe" languages, and optional objects could significantly enhance Ada in this regard.
An important advantage of using object-based ownership relative to pointer-based ownership is that ownership is based on containment rather than on a pointer "owning" what it points to. That is, the "owned" object is a component of the owner. Containment is already a well understood notion, and it is no surprise that when you copy an object, you copy all of its components, or when you reclaim an object, you reclaim all of its components, or when you pass an object as an in parameter, the called routine only has in access to its components. In an owning pointer model, each of these notions becomes a kind of special case, requiring "deep" copy, or "recursive" finalization, or "transitive" read-only access. The "new" idea with object-based ownership is simply that components can grow or shrink, or be absent, but the notion that an object "owns" its components is not new, and is already assumed by the programmer.
The other place where optionality comes up is when a scalar type has a special value used to indicate the notion of "unspecified" or "uninitialized". For example, using -1 for the age of a patient, or No_Value as the value of some object of an enumeration type, is a typical way of indicating missing or unspecified values. Having direct support in the language for the notion of an optional value means that there is no need to specify and agree on the name or value of this "unspecified" value, meaning that the programmer's intent is again clearer. Furthermore, a standard way for checking for such an "unspecified" value would be X /= null, and an automatic check would be provided to prevent such a special value from being used in an operation that expects a "normal" value. With the default being "not null" for non-access types, the programmer has to make an effort by using "null or" to indicate they want to allow null, which is again a way for the programmer to indicate their intent more clearly.
We propose that even in the presence of a specified Default_Value for a scalar type, an object of an optional subtype will still be initialized to the null value. This makes sense because the user has specified that null is to be used to indicate an optional object that is not present. The Default_Value serves a different purpose, presumably, as a reasonable initial value for an object that is present, but is not explicitly initialized.
The notion of an "option" or "maybe" type is provided by most languages these days (Java, Rust, OCaml, C++, ...), but it tends to be somewhat heavyweight, utilizing essentially a generic variant-record that wraps the value. We can certainly do that already in Ada, but for a lighter-weight solution in Ada, we can piggy-back on the notion of "null" already provided for access types, so we don't have the heavy extra syntax implicit in having a generic "option" type. With this approach, we can use the existing null exclusion mechanism already defined for access subtypes, but now allow it for all subtypes. The only new bit is having a "null inclusion" indicator, which we propose to be a prefix of "null or".
One big advantage of a general "optional" notion is that you can use it with any type, including one that never anticipated the need to have "optional" values. For example, there are times when what you really want is three-valued logic, comprising True, False, and <unknown>. Having to define a separate enumeration type for such a situation means that these three-valued-logic types proliferate. Furthermore, any use of such a type will be more awkward, because even if you are using the subtype that does not include the <unknown> value, you could not directly use it in an "if" statement; you would have to compare it explicitly against True or False. More generally, by making the notion of "optionality" standard, any user of a type can make use of optionality, even if the definer of the type did not anticipate the need for such a usage. Furthermore, the indication of whether a given component, parameter, result, or object is optional can be indicated at the point of use, with a standardized indicator (not null or null or as appropriate).
There might still be cases where programmers prefer to do it explicitly, with defining special values with named numbers or extra enumeration literals, and separate named subtypes for the one including vs. the one excluding the special value, but having a simple subtype-based mechanism is very consistent with current Ada, and is a lighter-weight approach in general.
Note to avoid confusion, we have replaced the term "null string" with "empty string", "null array" with "empty array", "null_array_aggregate" with "empty_array_aggregate", and the term "null constraint" with "empty constraint." These substitutions affect isolated parts of the RM – the terms mostly show up in places like the Array Aggregates section, or the String package descriptions. In fact, the term "empty string" shows up regularly in documentation instead of "null string", so Ada programmers already seem to treat them as synonyms.
We currently are proposing to allow a "null" literal to be of any type during name resolution. To avoid introducing too many ambiguities, we have a special Name Resolution rule that says an actual parameter in a call cannot be the "null" literal if the formal parameter subtype is a non-access subtype that disallows null. This Name Resolution rule depends on subtype information, but is relatively easily implementable given the "proximity" of formal parameter information (such as formal parameter names and presence of defaults) when doing overload resolution of calls. This avoids the need for passing down context through multiple levels to resolve a null literal.
The implementation expectation for representing a null value is that for scalar subtypes that don't use all bit combinations of their underlying representation, some bit combination that is not used would represent null. Different subtypes of the same type might use different bit combinations to represent null, and a different representation might be used in a packed representation vs. an unpacked representation. One possible choice for the "null" representation would be whatever value is used by the Normalize_Scalars pragma (RM H.1) to represent an invalid value for a given subtype. One issue with this is that were the user to compare an object with this "normalized" invalid value with null, it would then return True, whereas when the Normalize_Scalars pragma is omitted, there would be no such guarantee. This could be an argument to disallow comparing an object of a not-null subtype against null if this representation is chosen, or it could suggest that choosing different values for invalid and null would be a better approach. One advantage of using a different value for invalid would be that it could be easily recognizable during debugging, with a hexadecimal value of, for example, 16#DEAD#. Note however that even in the absence of Normalize_Scalars, it is possible that an uninitialized object might have a value that happens to match the null value, which is perhaps another argument for disallowing the comparison. If this idea is adopted by an implementation, then the Normalize_Scalars pragma could be defined to initialize the value for a non-optional object to the null value used for the corresponding optional subtype, unless it requires increasing the size, in which case Normalize_Scalars is generally relegated to initializing to some random legal value.
The 'Size of a subtype that allows null would reflect the size including the null value used in a packed context. The 'Object_Size of a subtype that allows null would reflect the size for an unpacked representation that includes null.
For scalar subtypes that do use all bit combinations of their representation, and for composite subtypes, an additional bit would be allocated (perhaps as a byte, or whatever additional amount is needed to satisfy alignment requirements). Hence, "null or Integer" would end up using at least Integer'Size+1 bits, while "null or Natural" would end up using Integer'Size bits (which is in fact Natural'Size+1). For float values, a particular NaN value might be reasonable. In the IEEE Float representation, the NaN that is all one's might be appropriate to pick for this purpose. Alternatively, if the Float subtype is full range and all NaNs need to be usable, then the extra bit approach might be necessary. There are precedents for using NaN for missing values. For example, the IDL standard recommends use of NaN rather than special values like -1.0 to ensure that the special values don't get used in operations unintentionally. See for example http://www.idlcoyote.com/math_tips/using_nans.html
For optional Unbounded objects (see AI22-0148-1 on unbounded objects), these are expected to be implemented similarly to an indefinite "holder" object, and these already have a notion of "Empty" which would naturally correspond to the null value. Even if the full "holder" mechanism isn't necessary, at a minimum there will generally be a level of indirection for unbounded objects, so that the null value is essentially a null pointer, and the non-null value is reached by dereferencing the pointer. This allows for recursive data structures, while retaining the containment model for components. If an implementation wants to use the same representation for every optional instance of a given composite type, independent of whether or not it happens to be a recursive use, then presumably a level of indirection would always be used for optional instances of a recursive type, or for that matter, optional instances of any (sufficiently large) composite type.
One concern is how to create safe "cursors" into a recursive data structure implemented using optional objects, such as a binary tree. If such cursors are needed, then given optional objects, an alternative representation for a tree is an indexed set of tree nodes, where the nodes are optional elements of a vector, and the indices into the vector are used for the Left and Right branches of a tree node, rather than directly holding the subtrees. The index is then also usable as a stable cursor into the middle of a tree. Even if a particular tree node is deleted (that is, set to null), the index remains valid, and the node deletion can be detected by comparing the node against null. If indices are going to be reused, then a cursor can be a combination of an index and a "generation" number, with generation numbers being stored in the vector as well, being bumped on each reuse of a slot in the vector.
As mentioned above, rather than using a special enumeration literal such as "No_Value" a simple specification of "null or" could be used:
type Part_Of_Speech is
(Noun, Pronoun, Verb, Adjective, Adverb,
Preposition, Conjunction, Interjection);
P : null or Part_Of_Speech; -- default initialized to null value.
Similarly for numeric values:
type Patient_Age is range 0 .. 150;
type Patient_Info is record
Name : Unbounded_String;
Age : null or Patient_Age; -- "null" means Age not present in
record
end record;
For components that are composite, "null or" combined with a potentially large component can take the place of an explicit access value, eliminating the need for storage management by the user. This is particularly useful for recursive data structures such as a linked list:
type Linked_List is record
Value : Element_Type;
Next : null or Linked_List;
end record;
and for components that are of an indefinite subtype (presuming we allow them – see AI22-0148-1 on indefinite subtypes):
type Address is record
Address1 : String := ""; -- required
Address2 : null or String; -- optional
City : String := ""; --
required
State_or_Province : String := ""; -- required
Postal_Code : null or String; -- optional
end record;
B-tests would be appropriate to test overload resolution for "null" in presence of subtypes that do and do not allow null.
C-tests would be appropriate to test default initialization of optional subtypes, to verify that Constraint_Error is raised if a null value is used with a predefined operator other than an equality operator.
This is related to ARG GitHub issue #149.
Randy Brukardt
12:16 AM April 28, 2026
This would make sense in a new language, but Ada has 40+ years of history with these terms. You are requiring compilers to change their error messages, internal comments, essentially invalidating all existing educational materials (as they would need to be changed), requiring changes in Ada handling tools (like ASIS and Libadalang), static analysis tools, and pretty much everything else. Moreover, no one is going to be able to search for "null" in any code, and longer phrases will be error-prone. Perhaps the RM can be changed cleanly (I'm skeptical), but the rest of the world is not so easy.
Tucker Taft
11:20 AM April 28, 2026
I am not too worried about the rest of the world, since these are not terms that are used that much (despite the number of paragraphs).
I think it is worth making these changes because the ambiguous use of the term "null" is going to be a continuing source of pain, while changing the RM is a one-time thing. And I would claim that many folks use, for example, "empty string" anyway, instead of "null string."
This also relates to the claim that after a good architect does a renovation, you can't see where the old building ends and renovated part begins.
Randy Brukardt
12:23 AM April 29, 2026
I understand that, which is why I originally thought that "null" is the wrong thing to use here. There is too much existing history with that term to just change it.
And I seriously do not know who is spending the time to change dozens of compiler messages, pages of documentation, and all of the rest that would need the same work. We don't have enough people to get stuff implemented in the first place, spending weeks redoing documentation is not going to happen.
Tucker Taft
8:58 AM April 29, 2026
Interestingly, if you do a search for 'GNAT "null string"' vs. 'GNAT "empty string"', there seem to be more matches with empty string. It seems Ada people happily treat them as synonyms.
Randy Brukardt
11:28 PM April 30, 2026
GNAT /= “Ada people”.
In any case, I am not signing up to do the vast amount of work you are requiring here. My retirement may be sooner than planned...
Randy Brukardt
11:21 PM May 4, 2026
Are you planning to change the name of the commonly used constant in Ada.Strings.Unbounded from Null_Unbounded_String to Empty_Unbounded_String???? Can you justify the incompatibility????[ae]
[a]The main problem I see in this AI is that it conflates two issues that should be considered separately: (1) optional objects, and (2) recursive types without access values. Both issues should be discussed, and perhaps lead to Ada extensions, but I do not think that their solutions are necessarily related, as indirectly suggested by this AI.
For optional objects, there has been public discussion between Ada adherents and adherents of languages which directly support optional objects, such as Rust, with two criticisms of Ada: (a) using variant records is too verbose, and (b) direct use of a discriminant-dependent component, with no "unwrapping", risks run-time errors. I think both criticisms are valid. Two solution proposals exist: (1) the approach in this AI, which adds a "null" value to all types; and (2) Github issue #169, which proposes a simpler syntax for some discriminated records, and allows for more than the null/non-null dichotomy. Neither proposal depends on recursive types.
Regarding recursive types without access values, to my knowledge Ada has not been criticized on this point, apart from e.g. comparing to Rust pointers. There seem to be three proposals: (1) per this AI, using recursive components that include null, and terminating the recursion with a null value; (2) per Github issue #176, using components of a record type where different variants either continue the recursion or terminate it (possibly with a non-null value); (3) per https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues/176#issuecomment-4859433633, using components of class-wide type where different derived types either continue or terminate recursion. Only the first proposal uses optional objects.
Thus, I think these two issues should be discussed separately, and their solutions may or may not be connected.
[b]I see a lot of synergy between the general notion of optionality and the desire to support recursive types more directly, and certainly once you have optionality, it would be a bit annoying to not allow it to support recursive types. From my perspective, optionality is a building block that solves several problems, so I think it is a feature that one concept can address two issues.
[c]I agree that if Ada has both (some form of) optional objects, and recursive types, then optional components should be allowed as one way of constructing recursive types. But I hope that I have shown in Github issue #169 that there is a simpler way to make optional objects easy in Ada, with only some syntactic sugar for discriminated records, and in Github issue #176 that there are natural recursive data structures that terminate in other ways, not needing and not suited for optional components, and instead using other kinds of multi-variant objects.
[d]The latest version of AI22-0148-1 permits a more general kind of non-infinite recursive type definition. We'll see if we can make it implementable!
[e]I feel that this is too limited a view of recursive data types, because there are recursive data types which are not terminated by the absence of a value (a null), and instead terminate with a non-recursive value.
I have suggested a more general approach in Github issue #176, https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues/176.
[f]This means that the complexity (time required) of an algorithm that builds up objects of a recursive type depends critically on whether the building proceeds top-down or bottom-up. For example, building a list starting from the head and appending new tail elements is linear in the number of elements, while starting from the tail and prepending new heads is quadratic, if the data are indeed copied.
Copy elimination will become an important compiler optimization. If the compiler can prove that there are no later uses of the object being assigned to the target, and the object is implemented with an implicit reference (pointer), it can just move the pointer without copying the data.
Should the RM require copy elimination in some (simple) cases? This would be similar to the requirement, in some functional languages, to implement tail recursion without stack growth.
Should there be a restriction to require copy elimination in all assignments of optional (recursive) components?
[g]The AARM could have an Implementation Note proposing an algorithm, but we generally don't make any requirements on the quality of Ada implementations.
Feel free to suggest the wording of such an AARM note.
[h]I think the difference between a linear-complexity implementation and a quadratic one, which could be quite surprising to programmers, is more than a difference in compiler "quality".
At least we should make it simple to use "move" semantics for assignments that build up recursive structures. I think you suggested something like that, did you not? But could it be made a requirement to offer that as programmer's choice? For example, require that any component C that is part of a recursive chain of components has a callable attribute C'Set_To (X) that performs C := X; X := null, using move semantics.
[i]I do mention Move and Swap operations in the original GitHub issue. I would suggest they be in a separate AI, since I suspect we will debate how best to represent those, and that is somewhat orthogonal from the decisions to be made in this AI. You might want to create a separate GitHub issue to discuss some of these performance concerns, since as Randy has pointed out, these Google Doc comments are not ideal for longer discussions.
[j]"For example, building a list starting from the head and appending new tail elements is linear in the number of elements, while starting from the tail and prepending new heads is quadratic, if the data are indeed copied."
The above example is definitely a case where a Move would be appropriate, since we are moving a component *within* the object, so we know the component is allocated from the correct storage area.
[k]Allow null-including subtype in case statement, [Done: but cannot use as an index subtype].
[l]Any subcomponent of an optional object that has any variable views is not legal for renaming. Think about generic in-out objects, and no lying.
[m]Controlling parameters/results must be explicitly null excluding (access types). First subtype of tagged private disallows null?
[n]Annex B -- C compatible types are null excluding only? But allow it?
[o]This notion of "null constraint" causes mostly trouble in the rest of the RM; if we HAVE to change it, the best thing would be to get rid of it altogether. (It's already confusing, and changing the name doesn't really help that.)
[p]There's nothing "null" or "empty" about such a constraint: it is the lack (absence) of any constraint at all that is being discussed. I don't understand what is being gained by giving that a name like it is some sort of constraint (which is necessarily confusing).
[q]We generally do not allow this sort of specification (outside of things associated with the container packages, and even then every paragraph needs to be specified). These should all be written out.
[r]Yes, I knew this was a cheat, but I think it might be easier to review in this way. Once we reach general agreement, I can expand these out.
[s]As noted above, at a minimum a list of paragraph numbers is required here.
[t]Doesn't this cause trouble even if the formal parameter allows null? If one tests the parameter for nullness, then the following checks are known to succeed and will be omitted in any reasonable compiler. If someone clobbers the parameter, then we have a problem. If that is NOT erroneous, then it is never possible to eliminate a check on a formal parameter passed by reference (which is most of them). Ugh.
[u]Note that the reason this problem doesn't appear for access types is that they are elementary and can never be passed by reference. The copy is never changed even if the actual is. One "solution" here is to require optional objects to have a level of indirection such that "null" is always passed by copy. But that of course makes any of your scalar cases impossible (I think they are impossible anyway because of private types and generic private types, but YMMV).
[v]I understand the problem you describe, but I believe this same problem exists for any component of any parameter that is passed by reference, and that is *not* currently considered erroneous, unless the update occurs due to erroneous concurrent access. If the update occurs as a side effect of something the called subprogram does (e.g. by calling an external procedure that changes the actual), that is allowed, and the called subprogram has to deal with it. That is, even though a parameter is mode "in", if it is passed by reference you have to be cognizant of the possibility that it will change due to some side-effect of an operation performed while the called subprogram is executing.
On the other hand, if the formal parameter *subtype* implies something about the existence of certain components, then the called subprogram can assume that those components continue to exist. It does seem like a bit of an arbitrary distinction, but I don't think we should change that.
[w]This seems to ignore the actual implementation of these things. A null allowing type will generally be a very anonymous access type (known only to the compiler) along with the actual (null excluding) type (often a record type).
When that very anonymous access type is passed as a parameter, it will be passed by copy; this will have the effect of passing the object the user (and language) knows about by reference. But as the pointer is passed by copy, nulling the original pointer will not be reflected in the formal parameter -- it will becoming dangling instead. That had better be erroneous.
To implement the model you have, one would have to pass all null allowing objects doubly indirect (as a pointer at a pointer), which is insane as it only would matter for the unusual aliasing cases.
[x]The reason that we make these things erroneous is so that a reasonable implementation is possible, rather than needing heroic efforts. You seem to be saying that is not allowed for null allowing objects, a performance sapping double indirection is required.
[y]OK, I am seeing your point now. Let me think about it further ...
[z]In any case, we seem to agree that it should be erroneous if it excludes null. The question is what to say if it includes null. If we treat this like an implicit access value, which is also unchecked deallocated as part of being set to null, then we blame the erroneousness on any use after unchecked deallocation. So setting an optional object to null should be treated similarly.
[aa]I have reworded things a bit.
By the way, as far as implementation, wouldn't you only pass the access value, rather than both the access value and a separate pointer to the (non-null) content? It will be hard to classify that as by-copy or by-reference. I suppose an unconstrained array is similar, in that it might have a part passed by copy (the bounds) and a part by reference.
[ab]Yes, you are only passing a single access value (for Janus/Ada, that's required, as parameters are fixed size). For unconstrained arrays, we pass a pointer at a descriptor containing the bounds and a ptr to the data (and for modifiable ones, also a ptr at the pool and a ptr at the finalization anchor).
[ac]I suppose if any part of the object is passed by reference, then we should consider it as "by reference" since if the whole object isn't passed by copy, then changes to the actual might affect at least some part of the formal.
[ad]I was thinking something something similar. By-copy passing should have no aliasing effects, and anything else is essentially by-reference (particularly for these erroneous execution rules).
[ae]I agree that would be painful. We could add a renaming as Empty_Unbounded_String, but removing the old name would be gratuitously incompatible. The hope is that this whole package might be of less use if the Unicode_Text stuff replaces it, but we will see.