AI22-0148-1

!standard 3.3.1(9/5)                                    26-07-15  AI22-0148-1/06

!class Amendment 25-11-02

!status work item 25-11-02

!status received 25-10-17

!assigned author Tucker Taft

!submitter Tucker Taft

!priority High

!difficulty Hard

!subject Bounded and Unbounded Indefinite Objects

!summary

An object declared with an indefinite nominal subtype may be specified as being of Bounded or Unbounded extent, which allows it to be reassigned other values with different constraints, a different tag, or more generally any other value satisfying the requirements of the nominal subtype. The notion of Extent is also defined for other unconstrained, definite subtypes (i.e. discriminated-with-defaults).

!issue

Ada currently specifies that objects with an "indefinite" nominal subtype (3.3(23/5))  are constrained by their initial value (3.3.1(9/5)), if permitted at all in a given context. There are generic packages Indefinite_Holders and Bounded_Indefinite_Holders which provide "holders" for objects of an indefinite subtype that allow the "held" value to be replaced by any other value of the given indefinite subtype, with the only limitation being, in the case of an instance of Bounded_Indefinite_Holders, on the overall size of the new value. Using these holders can involve substantial additional effort if used in multiple places, and does not permit using the content as naturally (or readably) in useful constructs such as assignment statements, parameter passing, aggregates, etc.[a]

For example, we would like to be able to do the following:

type Employee is record
    First, Middle, Last : String := "";
    Addr1, Addr2 : String := "";
    City : String := "";
    State : String(1..2) := "  ";
    Zip : String(1..5) := "00000";
end record;
type Emp_Id is new Positive;

package Employees is new Containers.Vectors(Emp_Id, Employee);
Company : Employees.Vector;
...
Company.Append
    ((First => "George", Last => "Washington",
      Addr1 => "3200 Mount Vernon Memorial Hwy",
      City => "Mt Vernon",
      State => "VA",
      Zip => "22121"));
...
for E of Company loop
    if E.First = "George" and then E.Last = "Washington" then
         E.City := "Mount Vernon";
    end if;
end loop;

 

As illustrated above, we would like to directly define the fields of the Employee record using Strings, without needing multiple discriminants to specify the length of each string.  The Employee record type as a whole would be treated as a "definite" type, just as though each of the String components were a Holder object, so we could instantiate Containers.Vectors rather than having to use Containers.Indefinite_Vectors.  We could loop over the elements of the Company vector directly, and could directly read and write individual String components using "normal" selected component notation.

Should we consider allowing the implicit insertion of "holder"-like semantics by declaring a component of an indefinite subtype, or by specifying an aspect on a stand-alone object of an indefinite subtype? (Yes)

!recommendation

We propose introducing two aspects, the "Extent" of an object, and the "Bound" of an object. The Extent of an object can be Constrained, Bounded, or Unbounded. The Bound[b][c] of an object has a value of subtype System.Storage_Elements.Storage_Count (13.7.1(4)). Depending on the nominal subtype of an object and its context of use, these aspects have default values, but they can be specified explicitly on an object, or on a subtype, to override these defaults.

Unlike the element of a holder, objects of an indefinite subtype declared with Extent Unbounded or Bounded must be initialized, unless they are specified as "optional" which will be the subject of a separate AI (AI22-0152-1).

If the nominal subtype of the object is not indefinite these aspects are still permitted, with essentially the same meaning, except that for objects of a definite but unconstrained nominal subtype (that is, discriminated-with-defaults), they would generally be by default Unbounded, and specifying Bounded or Constrained would change the semantics to be, correspondingly, bounded in size by the specified Bound, or constrained by their initial value.

These aspects may be queried on a subtype or object by referencing attributes Extent and Bound, including on a generic formal subtype. The Bound aspect is defined to be zero for an object or subtype with Extent Constrained, and Storage_Count'Last for an object or subtype with Extent Unbounded.

The default value for the Extent aspect is determined as follows, so as to match existing Ada semantics (where it was legal at all):

These aspects may be specified when declaring a new object or subtype, including of a generic formal type, and they override the incoming values of the aspects.  They may be specified directly on a generic formal type, in which case they must statically match the corresponding aspects of the actual, on instantiation.

These aspects may be specified for formal parameters, where they represent a limit on the incoming value provided, and on what changes the subprogram can make, presuming the parameter is of mode out or in out. They need not match the aspects of the actual parameter exactly, though they must be consistent. In particular, a formal parameter of Extent Constrained or Bounded can accept an actual parameter of any Extent. An Extent of Unbounded can accept only actuals that are unconstrained (and Unbounded), if the nominal subtype is unconstrained.  If a Bound is specified for a formal, it must be no more than that of the actual for mode out or in out, and the actual size of the actual must be no more than the formal parameter's Bound.

If a subcomponent of a type has extent Unbounded, we would like it to be possible to nevertheless bound the total space occupied by an object of the type, including its Unbounded subcomponents. So the proposal is that having an Unbounded subcomponent effectively makes the enclosing type indefinite, and the Extent of an object of that enclosing type can impose an overall Bound. What this means is that the Unbounded subcomponents of a type would be allocated from a storage area[e][f][g][h][i] determined by the storage area where the object as a whole is allocated. For example, if the object as a whole is created by an allocator for an access type with a specified Storage_Pool, all of the Unbounded subcomponents would also be allocated from this Storage_Pool. If the object as a whole is on the stack, and a Bound is imposed on that object, then effectively a local Storage_Pool with the given Storage_Size would be created, as though the object were designated by an access type with a specified Storage_Size, and the object and its subcomponents would be allocated out of that local "sized" Storage_Pool.

To address the issue associated with AI22-0140-1[j], we suggest that the Extent aspect can take the place of the aspect Constrained proposed in that AI. In particular, by specifying the Extent aspect on a formal parameter or a designated subtype, we can override the default value for Extent (defined above) to give the desired effect. By specifying a formal parameter is Constrained we promise the subprogram will not change the constraints of the actual, and by specifying a formal parameter is Unbounded, the actual must also be unconstrained (and Unbounded). Similarly, by specifying a designated subtype as Unbounded, the objects created by an allocator of an access type with such a designated subtype, the allocated objects will be unconstrained. A value of such an access type could not be converted to a constrained access subtype.

!wording[k][l][m]

Modify 3.3(23/5):

At the place where a view of an object is defined, a nominal subtype is associated with the view. The nominal type of a view is the type of the nominal subtype of the view. The object's actual subtype (that is, its subtype) can be more restrictive than the nominal subtype of the view; it [always ]is more restrictive if the nominal subtype is an indefinite subtype{, unless the Extent aspect is Unbounded (see below)}. A subtype is an indefinite subtype if it is an unconstrained array subtype, or if it has unknown discriminants or unconstrained discriminants without defaults (see 3.7); otherwise, the subtype is a definite subtype [Redundant: (all elementary subtypes are definite subtypes)]. [Redundant: A class-wide subtype is defined to have unknown discriminants, and is therefore an indefinite subtype. An indefinite subtype does not by itself provide enough information to create an object; an additional constraint or explicit initialization expression is necessary (see 3.3.1).[ A component cannot have an indefinite nominal subtype.]]

Add after 3.3(23.13/5):

Given an object (including a component) of an unconstrained composite subtype, the following aspects may be specified:[n]

Extent

This aspect shall be specified as one of Constrained, Bounded, or Unbounded. If the aspect is Constrained, the object is constrained by its initial value. If the aspect is Bounded or Unbounded, the object is not constrained by its initial value. If the aspect is Bounded, the Bound aspect shall be specified for the object as well (see below). If the aspect is Constrained, the Bound aspect is zero, and only a confirming aspect specification is permitted for Bound. Similarly, if the aspect is Unbounded, the Bound aspect is System.Storage_Elements.Storage_Count'Last, and only a confirming specification is permitted for Bound.

Bound[o]

This aspect is of type System.Storage_Elements.Storage_Count. If statically specified as zero, the Extent aspect is by default Constrained. If statically specified as Storage_Count'Last, the Extent aspect is by default Unbounded. If any other value is specified, or the value is specified with a nonstatic expression, the Extent aspect is by default Bounded. When the Extent aspect of the object is Bounded, the Bound aspect represents an upper bound on the number of storage elements that the object is allowed to occupy. On initialization and on any assignment to the object, a check is made that the object does not exceed this limit. If this check fails,[p][q] Storage_Error is raised.

The above aspects may be specified for an unconstrained composite subtype, in which case it represents the default value for the corresponding aspect of any object of the subtype.[r]

Modify 5.2(10):

Otherwise [Redundant: (the expression is dynamically tagged)]{, if the object is constrained by its initial value}, a check is made that the tag of the value of the expression is the same as that of the target; if this check fails, Constraint_Error is raised.{ If the object is not constrained by its initial value, a check is made[s][t] that the tag of the value of the expression matches that of the target, or the master of the tag of the new value includes that of the nominal type of the target; if this check fails, Program_Error is raised[u][v][w][x]

AARM Reason: This check ensures that the tag of the new value of the object matches that of the old value, or identifies a type that lives at least as long as the nominal type of the object.}

Delete 5.2(15):

NOTE   The tag of an object never changes; in particular, an assignment_statement does not change the tag of the target.

Modify 6.4.1(18/3):

If the nominal subtype of a formal parameter {of a composite type is constrained, or is constrained by its initial value}[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{, array bound, or tag} of the actual is changed while the formal parameter exists (that is, before leaving the corresponding callable construct).

!discussion

Dealing with objects of an indefinite subtype is pretty painful in Ada, and Holders were invented to give more flexibility in how to deal with them. Holders have proved to be useful, but having to instantiate a generic, and then use various subprograms to do anything interesting with such objects is both less convenient and less readable and understandable than direct manipulation.

Ada allows objects of a discriminated type to be manipulated relatively directly, and reassigned arbitrary new values, but only if all of the discriminants have defaults. This connection between defaults and re-assignability is not obvious to the new user of Ada, and seems a bit odd even to an advanced user of Ada. By making the notion of Extent explicit, we believe that we can give more flexibility, and make the program both more convenient and more understandable.

The notion of Extent can also be used to address the issue associated with AI22-0140-1, where Extent => Constrained or Extent => Unbounded can make the distinction necessary to eliminate the need for a run-time check on the 'Constrained attribute, and also allow objects created by allocators to be unconstrained, without worrying about whether or not they have a partial view without visible discriminants. Here again, we believe that an explicit use of an aspect like Extent is better than the somewhat obscure rule used currently, where being unconstrained in the heap depends on the presence of a particular partial view.

The notion of bounding the storage used by a given object in Ada has emerged as an important feature of the standard containers, and bringing the general notion of bounding storage for an object of a type, which in the worst case might take up all of memory (such as String, or a type with a discriminant of subtype Positive), seems valuable. Currently the user has to carefully choose subtypes for the discriminants of a discriminated type, and then give them defaults, to get a bounded object of a type with discriminants. This seems quite indirect, and the actual desired bound on storage is never made explicit. By allowing a specification of a Bound on any object of an unconstrained subtype, we give the embedded-systems programmer more flexibility, while also making their intent more obvious and understandable.

In a separate AI we will propose an attribute function 'Storage(discrim1, discrim2, …) or 'Storage(length1, length2, …) so that when specifying a Bound,[y][z][aa] the user can specify the discriminants or dimension lengths of the largest object they want to be able to represent in the storage area for the object.

We could reduce redundant aspect specification by saying that if a Bound aspect is specified to be neither zero nor Storage_Count'Last, then the Extent aspect is automatically defined to be Bounded, which can of course be confirmed with a redundant specification of Extent => Bounded if desired.

If we make the handling of unbounded components fully automatic, then one could argue that the only remaining "visible" restriction associated with an "indefinite" type is that it requires an initial value. Presuming we require that all unbounded subcomponents of a private type have default initial values, then there is no particular reason a private type with one or more unbounded components has to be "visibly" indefinite. This might also imply that an existing type like Unbounded_String could be defined by simply having a single unbounded String component with a default initial value of the null string, letting the compiler do all the work of worrying about implicit levels of indirection, etc. This would mean that the existing implementation of those language-defined types that do all of their own storage management, could be simplified so they rely on the compiler's implementation of unbounded components.[ab][ac]

We allow incomplete views as components, given that almost anything can be a component now. We do require that the type of the incomplete view be fully defined before the enclosing composite type is frozen, so reasonable representation decisions can be made.

We also now allow a type to be recursive (see 3.2.1(5) above), so long as the recursive occurrence appears directly or indirectly within a variant part that is not constrained to always exist. If we approve the AI on optional objects (AI22-0152-1), then we also allow recursion so long as the recursive occurrence appears directly or indirectly within an optional component. In general, allowing a recursive type definition is OK, so long as there is no possibility of an infinite recursion.  See AI22-0169-1 (More Flexible Recursive Types) for more discussion of this issue.

!example[ad]

VS : String := "" with Extent => Unbounded;
...
if X > Y then
   VS := @ & " add onto end";
end if;

 

As an optimization, an implementation might recognize a case like this where we are appending onto the end of a string, and if there is already sufficient adjacent empty space in the storage area in use for the string, the new characters could be moved directly into this empty space.  Presumably for an Unbounded object, as the object grew, the implementation would over-allocate, perhaps by doubling the allocated space on each expansion, so it would not be uncommon that there is adjacent empty space.

This sort of optimization could be difficult to do when explicitly using a generic such as Indefinite_Holders, but is easier to imagine for a construct like this using predefined operations.  Implementations often already have special builtins for handling operators like concatenation, and an appending such as this might also get its own builtin.[ae][af]

By allowing "unbounded" (or "optional") objects as components, rather than requiring the programmer to introduce explicit pointers, or use a pointer-based data structure like a Vector container, the language is directly supporting the notion of object "ownership", where "managed" components are associated closely with an enclosing object, and when the enclosing object goes away, the "managed" component does as well. A "managed" component is only ever a "part" of a single enclosing object, so there is no concern about dangling references or storage leakage. If you are interested in more discussion about object and pointer ownership, you might find the "Related Work" section (page 21) of this paper on ParaSail of interest, which talks about avoiding pointers while supporting flexible data structures: https://arxiv.org/abs/1902.00525. The Rust language and the SPARK subset of Ada both support pointer ownership, but direct object ownership is a somewhat simpler model for the programmer, with many of the same capabilities.

We have prepared a presentation on the concept of object ownership, which is here. One advantage of pointer ownership vs. object ownership, is that, if pointers are being used as "cursors" into a complex data structure, the pointer ownership model will automatically transfer ownership to this pointer-based cursor as long as it exists, preventing concurrent updates to the original data structure until the cursor goes away.  AI22-0131-2, which proposes a Part_Of aspect, combined with AI22-0075-1 Aliased Results, can provide some of this sort of safety. An alternative approach is to represent the data structure using an indexed set, where the indices into the indexed set can be used to provide the equivalent of a pointer-based cursor.  An index-based cursor ensures that even if the data structure is modified, the cursor can safely be "dereferenced" and it will continue to track the element of the data structure as long as the element exists, and will indicate when the element has been deleted by finding that the index-based cursor (which might include a "generation number") no longer refers to any element in the structure.

!ACATS test

Both B-tests (legality) and C-tests (execution) would be appropriate here.  Bounds could be tested with simple cases.

!appendix

The discussion that led to this AI can be found in ARG GitHub issue #148 (some unrelated ideas are found there as well).


 

Randy Brukardt

10:29 PM Dec 5 2025

This is not a complete solution to this problem, however, as the need for references is not covered. If one reverts to using aliased objects, you reintroduce the dangling pointer problems that need to be solved. Access type ownership is needed in those situations (think map container cursors as an example).

Tucker Taft

10:11 PM Dec 10 2025

Cursors are certainly a challenge, but no more so than currently, where they can become invalid.  

One alternative approach would be to put a counter in containers that is bumped whenever a significant change occurs, and have cursors and other references keep a copy of that counter, and complain if it doesn't match on use.  This would move the place where tampering is detected, but would remove the need for cursors or references to be controlled, which would probably be a good thing.  We could allow tampering to be detected later, without requiring the existing checks be eliminated.

Randy Brukardt

1:19 AM Dec 11 2025

There is nothing "alternative" about that approach; it's how my implementation of Ada.Containers (will) work. I've made sure that implementation was and is legal throughout the container definition. But it's not a complete solution, because we have cursor-only operations. If the underlying container ceases to exist, and a cursor-only operation is called on a cursor that designates that container, the check you are suggesting is necessarily erroneous. Ergo, a dangling cursor of that kind cannot be reliably detected. IMHO, there never was any reason for cursors to be controlled, the cases that fail are rare enough not to worry much about. But if one wants perfect detection, one needs some sort of ownership scheme. (Or containers without cursor-only operations, which would be quite incompatible for Ada, sadly.(

Tucker Taft

9:12 AM Dec 11 2025

I think we can agree that cursor-only operations are fundamentally unsafe right now, and will probably continue to be so in the future.  We could mark them as obsolescent, perhaps ...

Tucker Taft

11:09 AM Mar 6 2026

By the way, you imply that the check for tampering with cursors can be delayed until the dangling cursor is used.  My reading of, for example, RM A.18.4(7/5) is that Program_Error must be propagated by a call that, say, deletes an element, and can't be delayed until there is a use of a cursor that refers to a deleted element.

Randy Brukardt

7:41 PM May 6 2026

I wasn’t talking about tampering at all. Cursors can be stored indefinitely (long after any tampering check has expired, even long after the object itself is gone). The only limit on the lifetime of a cursor is that of the instantiation that created the type. A cursor that no longer designates its original element is considered “invalid” or “ambiguous”, and using such a cursor is a bounded error or in some cases, erroneous. See, for example, A.18.2(240-252).


 

Randy Brukardt

2:50 PM July 3, 2026

The proposed change to 3.2.1(5) seems to allow an example like:

   type T (B : Boolean) is record

       case B is

           when True =>

                C : T(B);[ag]

           when False =>

                null;

       end case;

   end record;

   Obj : T(True);

Obj has an infinite number of C subcomponents, as each C component has a C subcomponent. The Extent of component C is Constrained; I don’t see how that could ever work. It might work for Unbounded or Bounded constraints, but only if the default discriminant value does not select the variant in question.

This seems too complicated to define, and probably only marginally useful, compared to optional components.


 

[a]Provide more motivating examples of the value of these ideas.

Make a link to the slides, and put them somewhere.

[b]This is a pretty general word, especially if Bound can be specified without also specifying Extent => Bounded. Why not use the existing name "Storage_Size"? The meaning seems to be much the same as for the Storage_Size of tasks and pools.

[c]The intent was to link the term "Bound" to "Bounded", but "Storage_Size" is certainly a reasonable alternative.

[d]Nested extension accessibility checks only needed in certain places because a class-wide object can't change tags.  This could break that assumption.  See the GNAT extension for mutable 'Class.

Renaming rules might be relevant here.

[e]Since the model here is that there is a storage pool associated with wherever this is defined (including the stack), can't we just say that the storage is allocated from the appropriate storage pool and not introduce this term here?

[f]Perhaps, though storage pools currently are for objects created by allocators.  Also, the intent is that you can specify that a component is bounded, and that all subcomponents of that component should be allocated out of this "sub"-storage area.  This could be fully hierarchical.  Storage pools have the notion of subpools, but they fundamentally only provide a single level of "nesting".

[g]??? There's no "nesting" needed for the memory of a single object: all of the memory comes from a single source (the pool). Given that the pieces could be allocated at different times (because of component re-assignments), I don't see any approach that would assure any sort of locality of the pieces -- other than those that are directly allocated in the stack frame, and those obviously don't need any special management to deallocate. (This is true of most potential uses of subpools as well, they don't buy much in most practical use cases.)

[h]The nesting comes in when you have bounded subcomponents.  Everything "below" a bounded subcomponent needs to live within the storage bound established at that point.  And you can have another bounded subcomponent within a given bounded subcomponent, so in this way you get nesting, with potentially smaller and smaller bounds at each "bounding" level.

They are all coming from the same overall storage area, but at each level of nesting there is a new bound imposed on everything below that point.

[i]Humm, I don't think this works very well. I've written a detailed message on this point and put it into the Github issue (it's way too large for this message box).

[j]Say more about checks performed at the call site.  Take a look at GNAT extension that permits mutable 'Class objects for ideas.

[k]Make sure that if the object is *not* optional, finalization cannot release the memory while the object still exists.

[l]Probably need restrictions on renaming of discriminant dependent and is part of an object that is *not* constrained by its initial value.

[m]Also, need to handle what AI22-0140-1 was trying to accomplish in terms of controlling whether the target of an access type is constrained by its initial value, among other things.

[n]Need to specify default values for Extent and Bound in absence of an explicit specification.  Need an attribute for testing this on generics and parameters.

[o]Should this be Storage_Size or Capacity (but that is used for limiting the number of elements)?

[p]Need to keep a running total of all subcomponents at each level with a bound.

[q]If there are unbounded subcomponents of a bounded object, you have effectively turned it into a storage pool, which can have fragmentation issues.

Include AARM note to give guidance about fragmentation.

[r]Would be a nuisance if you couldn't do this.  What does it mean if you use it other than to define an object (e.g. as a prefix of an attribute?)

[s]Do we need a similar check as this when an aggregate is constructed? That's definitely not an assignment statement.

[t]I can't off-hand think of any other way for this to happen.

[u]Typically, we've had a matching static check for such cases (against the nominal subtype of the target).

[v]Overload resolution already handles that check.

[w]I'm skeptical: resolution isn't enough for returns and allocators; why is this different??

[x]This is different because it allows the tags to match, which means that a static check beyond what is done by overload resolution is difficult to perform.

[y]Tuck should get started on this AI.

[z]_Marked as resolved_

[aa]_Re-opened_

Not resolved actually.  We need an AI for this.  Probably do not want to use same name for this attribute and the attribute for sharing storage between objects.

[ab]It would seem that we could make the bounded containers obsolescent, if users can set a bound on the usual unbounded containers (like vectors). Similarly, the Bounded_Strings could be made obsolescent if users can set a bound on Unbounded_Strings. In both cases, using a discriminant or generic formal for a capacity (as in those existing packages) is less flexible than this proposal and the existing unbounded containers.

I note in passing that if we do that, the rather complex mechanism in AI22-0124-1, which mostly is intended to make bounded container aggregates more useful, probably isn't necessary and could be omitted from the revision.

[ac]I agree that we could avoid having separate bounded-this and bounded-that if there is a general capability for bounding objects.

[ad]Example should be more convincing, so we don't already have Unbounded versions.  Also want more interesting examples, including ones where the Bound is specified.

[ae]But we are proposing (mostly) replacing String with a new U_String type (which would be more useful for non-English languages and the use of Emojis), so this sort of optimization would only make sense for legacy code -- except of course that legacy code wouldn't be using this new mechanism. So this seems like a waste of everyone's time.

[af]I suspect that plenty of new code will continue to use Standard.String.  But in any case, this is a possible optimization, and implementors can decide whether to implement it.

We could go further if we think "X := @ & Y" will generally be a common operation, and define an aspect to provide a concatenate *procedure* which would be called when the ":= @ &" combination occurs for a user-defined type.  This is analogous to the procedure option for reducers.  Essentially we could allow the user to define a "&=" procedure.

[ag]I have discussed this issue further in AI22-0169-1 More Flexible Recursive Types, and this case would be disallowed.