Going beyond Ada 2022
by Arnaud Charlet –
As we've seen previously in Ada 2022 support in GNAT, the support for Ada 2022 is now mostly there for everyone to take advantage of. We're now crossing fingers for this new revision to be officially stamped by ISO in 2022.
In practice, making new ISO revisions of the language is a long process, which so far happens roughly every ten years. On our side, following the general evolution of the language design culture, and some feedback we've received from our community of users, we're looking to have a shorter and more lively feedback loop.
This is why we have started in 2019 a new initiative, centered around the Ada/SPARK RFCs platform, which will allow us to experiment with new language features for Ada.
With this platform, we want to give anyone an opportunity to propose language evolutions through RFCs ("request for comments"), discuss the merits of RFCs publicly with the community, select those that are the most promising for prototyping, prototype the features in GNAT (and/or SPARK as appropriate), gather feedback from users of the feature, and depending on that feedback, either abandon the feature, modify it, or keep it. Finally when relevant, propose the features we kept for inclusion in the next version of Ada to the Ada Rapporteur Group, the international body in charge of Ada standardization.
In order to assess the needs of current and future users of the Ada programming language, we asked people inside and outside AdaCore what they wish for the future of Ada and SPARK. We got a lot of insights from these answers, coming from programmers with different backgrounds. One of the most common request is to get more compile-time guarantees, in areas such as data initialization before use, access to discriminated fields, dereference of possibly null pointers, dynamic memory management. Note that SPARK already offers such guarantees, at the cost of constraining the language and requiring an analysis which is much more costly than compilation. Here, our goal will be to provide the guarantees above for Ada programs through simpler compilation. Other common requests were: more powerful generics with richer specifications and implicit instantiation, better string handling that properly supports unicode, a more universally available mechanism for data finalization as well as some frequently requested syntax additions. The peculiar object model in Ada turned out to be a contentious issue, with some strong supporters and strong opponents, who advocated for rebuilding it more alike to the dominant model of Java/C++.
Thanks to the suggestions received so far from both external contributors and from the language design team at AdaCore, we have started adding some of these new experimental features, with a first implementation available in the latest GNAT Community 2021 release as well as the latest GNAT Pro 22 Continuous Release, under the -gnatX switch and detailed below.
Most Wanted Features
Let's first start with two "most wanted" features that many Ada users have been asking for years:
Fixed Lower Bound
Detailed in RFC#38, you can now specify a lower bound for unconstrained arrays that is fixed to a certain value.
Use of this feature increases safety by simplifying code, and can also improve the efficiency of indexing operations.
For example, a matrix type with fixed lower bounds of zero for each dimension can be declared by the following:
type Matrix is array (Natural range 0 .. <>, Natural range 0 .. <>) of Integer;
Objects of type Matrix declared with an index constraint must have index ranges starting at zero:
M1 : Matrix (0 .. 9, 0 .. 19);
M2 : Matrix (2 .. 11, 3 .. 22); -- Warning about bounds; will raise CE
Similarly, a subtype of String can be declared that specifies the lower bound of objects of that subtype to be 1:
subtype String_1 is String (1 .. <>);
If a string slice is passed to a formal of subtype String_1 in a call to a subprogram S, the slice’s bounds will “slide” so that the lower bound is 1. Within S, the lower bound of the formal is known to be 1, so, unlike a normal unconstrained String formal, there is no need to worry about accounting for other possible lower-bound values:
procedure Str1 is
subtype String_1 is String (1 .. <>);
procedure Proc (S : String_1) is
begin
-- S'First = 1
Put_Line (S);
end Proc;
S : String_1 := "hello world";
begin
Proc (S (7 .. S'Last));
-- sliding on S (7 .. S'Last) occurs automatically when calling Proc,
-- so this will pass a String_1 (1 .. 5) whose content is "world"
end Str1;
Generalized Object.Op Notation
Detailed in RFC#34, the so called prefixed-view notation for calls is extended so as to also allow such syntax for calls to primitive subprograms of untagged types. The primitives of an untagged type T that have a prefixed view are those where the first formal parameter of the subprogram either is of type T or is an anonymous access parameter whose designated type is T. This is another "most wanted" feature since the introduction of this notation in Ada 2005! For example:
generic
type Elem_Type is private;
package Vectors is
type Vector is private;
procedure Add_Element (V : in out Vector; Elem : Elem_Type);
function Nth_Element (V : Vector; N : Positive) return Elem_Type;
function Length (V : Vector) return Natural;
...
end Vectors;
package Int_Vecs is new Vectors(Integer);
V : Int_Vecs.Vector;
...
V.Add_Element(42);
V.Add_Element(-33);
pragma Assert (V.Length = 2);
pragma Assert (V.Nth_Element(1) = 42);
Additional "when" Constructs
This smaller syntactic addition discussed in RFC#73 adds the ability to use the "when" keyword to "return", "goto" and "raise" statements, in addition to the existing "exit when" control structure.
For example:
procedure Do_All (Element : access Rec; Success : out Boolean) is
begin
raise Constraint_Error with "Element is null" when Element = null;
Do_1 (Success);
return when not Success;
Do_2 (Success);
return when not Success;
Do_3 (Success);
return when not Success;
Do_4 (Success);
end Do_All;
Pattern Matching
This feature on the other hand (detailed in RFC#50) is a large one and is still being worked on. It provides an extension of case statements to cover records and arrays, as well as finer grained casing on scalar types and will in particular in the future provide more compile time guarantees when accessing discriminated fields.
For example, you can match on several scalar values:
type Sign is (Neg, Zero, Pos);
function Multiply (S1, S2 : Sign) return Sign is
(case (S1, S2) is
when (Neg, Neg) | (Pos, Pos) => Pos,
when (Zero, <>) | (<>, Zero) => Zero,
when (Neg, Pos) | (Pos, Neg) => Neg);
Matching composite types is currently only supported on records with no discriminants. Support for discriminants and arrays will come later.
The selector for a case statement may be of a composite type. Aggregate syntax is used for choices of such a case statement; however, in cases where a “normal” aggregate would require a discrete value, a discrete subtype may be used instead; box notation can also be used to match all values.
Consider this example:
type Rec is record
F1, F2 : Integer;
end record;
procedure Match_Record (X : Rec) is
begin
case X is
when (F1 => Positive, F2 => Positive) => Do_This;
when (F1 => Natural, F2 => <>) | (F1 => <>, F2 => Natural) => Do_That;
when others => Do_The_Other_Thing;
end case;
end Match_Record;
If Match_Record is called and both components of X are Positive, then Do_This will be called; otherwise, if either component is nonnegative (Natural) then Do_That will be called; otherwise, Do_The_Other_Thing will be called.
If the set of values that match the choice(s) of an earlier alternative overlaps the corresponding set of a later alternative, then the first set shall be a proper subset of the second (and the later alternative will not be executed if the earlier alternative “matches”). All possible values of the composite type shall be covered.
In addition, pattern bindings are supported. This is a mechanism for binding a name to a component of a matching value for use within an alternative of a case statement. For a component association that occurs within a case choice, the expression may be followed by “is <identifier>”. In the special case of a “box” component association, the identifier may instead be provided within the box. Either of these indicates that the given identifier denotes (a constant view of) the matching subcomponent of the case selector.
Consider this example (which uses type Rec from the previous example):
procedure Match_Record2 (X : Rec) is
begin
case X is
when (F1 => Positive is Abc, F2 => Positive) => Do_This (Abc);
when (F1 => Natural is N1, F2 => <N2>) |
(F1 => <N2>, F2 => Natural is N1) => Do_That (Param_1 => N1, Param_2 => N2);
when others => Do_The_Other_Thing;
end case;
end Match_Record2;
This example is the same as the previous one with respect to determining whether Do_This, Do_That, or Do_The_Other_Thing will be called. But for this version, Do_This takes a parameter and Do_That takes two parameters. If Do_This is called, the actual parameter in the call will be X.F1.
If Do_That is called, the situation is more complex because there are two
choices for that alternative. If Do_That is called because the first choice
matched (i.e., because X.F1 is nonnegative and either X.F1 or X.F2 is zero
or negative), then the actual parameters of the call will be (in order)
X.F1 and X.F2. If Do_That is called because the second choice matched (and
the first one did not), then the actual parameters will be reversed.
Simpler Accessibility Rules
This one (see RFC#47) is still a moving target and would definitely welcome some user experimentation and feedback! It starts with the observation that over the years, the rules that govern accessibility in Ada, that is, what operations on pointers are allowed, have grown to a point where they are barely understood by implementers and even less so by users. So by introducing a new restrictions pragma, we want to both simplify the rules and propose a model where runtime accessibility checks related to the use of anonymous access types are suppressed and replaced by compile time checks, in particular because the runtime accessibility checks are either impossible to implement fully, or worse, may produce false alarms (raising an exception in cases where no dangling access is actually occurring).
So if you add as part of your configuration pragmas the following:
pragma Restrictions (No_Dynamic_Accessibility_Checks);
This will enable this new mode. Currently two variants are implemented:
Designated type model
The default model when using No_Dynamic_Accessibility_Checks in GNAT Community Edition 2021 although in the more recent GNAT Pro development version, we've switched this model with the other one and this one will be enabled via the -gnatd_b debug switch, in addition to the restriction.
In this model, anonymous access types are implicitly declared at the point where the designated type is declared.
Point of declaration model
Available via the additional use of the -gnatd_b switch in GNAT CE 2021 and by default when using No_Dynamic_Accessibility_Checks in more recent GNAT Pro versions, the anonymous access types are implicitly declared at the point where the anonymous access is used (as part of a subprogram parameter, object declaration, etc...).
Both models may be refined further based on the feedback received on actual code and what users would find most useful and practical, so do not hesitate to give it a try and let us know!
Next Steps
Do you find some of these features useful? Do you want to give them a try and tell us what you think? Do you have some ideas for other new Ada features or other changes to existing features?
We encourage you to give it a try, give your feedback, and make new suggestions in the Ada/SPARK RFC platform! On our side we'll continue prototyping other RFCs and refine existing ones, so stay tuned.