AI22-0118-1

!standard 4.10(7/5)                                   25-04-24  AI22-0118-1/04

!standard 4.10(16/5)

!standard 4.10(21/5)

!class binding interpretation 24-10-24

!status Amendment 1-2022  24-11-04

!status WG9 Approved 25-07-18

!status ARG Approved  12-0-2  24-10-31

!status work item 24-10-24

!status received 23-12-22

!submitter Christoph Grein

!priority Medium

!difficulty Medium

!qualifier Error

!subject Put_Image for derived types with new discriminants

!summary

When an untagged derived type defines a new discriminant part, Put_Image should reflect the new number of discriminants, and their names.

!issue

Discriminants can be effectively replaced or renamed in a derived type. See for example RM 3.7(34-35):

type Matrix_Rec(Rows, Columns : Integer) is
   record
      Mat : Matrix(1 .. Rows, 1 .. Columns);    -- see 3.6
   end record;

type Square(Side : Integer) is new
   Matrix_Rec(Rows => Side, Columns => Side);

 

The current rules for Put_Image do not seem to properly handle this case for untagged derived types, and result in an image that still has the two discriminants Rows and Columns rather than the single Side discriminant.

Should the rules for Put_Image be adjusted to properly handle this case? (Yes.)

!recommendation

For tagged type extensions RM 4.10(16/5) specifies:

For a nonnull type extension, the default implementation of T'Put_Image depends on whether there exists a noninterface ancestor of T (other than T itself) for which the Put_Image aspect has been directly specified. If so, then T'Put_Image will generate an image based on extension aggregate syntax where the ancestor type of the extension aggregate is the nearest ancestor type whose Put_Image aspect has been specified. If no such ancestor exists, then the default implementation of T'Put_Image is the same as described below for a nonderived record type.

But for an untagged derived type RM 4.10(7/5) specifies:

For an untagged derived type, or a null extension, the default implementation of T'Put_Image invokes the Put_Image for its parent type on a conversion of the parameter of type T to the parent type.

This would produce an image for the Square type above displaying Rows and Columns rather than Size for the discriminants. To avoid this, there should be a rule similar to that for tagged type extensions, and in fact is probably most simply handled by adjusting paragraph (7/5) to only apply to derived types without new discriminants or nonnull extensions, and updating paragraph (16/5) to include derived with new discriminants.

Paragraph (21/5) would also need some minor rewording to cover the new cases when the default image implementation is used for certain composite derived types.

!wording

Modify 4.10(7/5):

For an untagged derived type{ declared without a known_discriminant_part}, or a null extension, the default implementation of T'Put_Image invokes the Put_Image for its parent type on a conversion of the parameter of type T to the parent type.

Modify 4.10(16/5):

For a nonnull type extension{, or an untagged derived type declared with a known_discriminant_part}, the default implementation of T'Put_Image depends on whether there exists a noninterface ancestor of T (other than T itself) for which the Put_Image aspect has been directly specified. If so, then{:

If no such ancestor exists, then the default implementation of T'Put_Image is the same as described below for a nonderived record {or protected} type.

Modify 4.10(21/5):

For a record type (or, as indicated above, [a type extension with no noninterface ancestor specifying Put_Image]{certain composite derived types}), or a protected type, the default implementation of T'Put_Image generates an image based on named (not positional) record aggregate syntax (except that for a protected type, the initial left parenthesis is followed by "PROTECTED with "). Component names are displayed in upper case, following the rules for the image of an enumeration value. Component values are displayed via calls to the [component type's ]Put_Image procedure{ of the component type}.

!discussion

When a known_discriminant_part is given in the declaration of a derived type, the discriminants effectively replace those of the parent type. The Put_Image should reflect the names, and the number of, these new discriminants.

Note that this is an inconsistency with the prior definition of Put_Image.

!example

Here are examples of derived types declared with known_discriminant_parts where this proposal will change the result of Put_Image:

type Square (Side : Integer) is new
   Matrix_Rec(Rows => Side, Columns => Side);

type NT (B : Boolean) is new T(D => B);

 

Note that a type that has "null record" as its extension_part is not considered a "null extension" if it has a new known_discriminant_part, so the existing rule in (16/5), which used the term "nonnull extension", already included a type such as:

type NN (X : Discrim) is new N with null record;

So such a type was already handled correctly

!corrigendum 4.10(7/5)

@drepl

For an untagged derived type, or a null extension, the default implementation of T'Put_Image invokes the Put_Image for its parent type on a conversion of the parameter of type T to the parent type.

@dby

For an untagged derived type declared without a @fa{known_discriminant_part}, or a null extension, the default implementation of T'Put_Image invokes the Put_Image for its parent type on a conversion of the parameter of type T to the parent type.

!corrigendum 4.10(16/5)

@drepl

For a nonnull type extension, the default implementation of T'Put_Image depends on whether there exists a noninterface ancestor of T (other than T itself) for which the Put_Image aspect has been directly specified. If so, then T'Put_Image will generate an image based on extension aggregate syntax where the ancestor type of the extension aggregate is the nearest ancestor type whose Put_Image aspect has been specified.

If no such ancestor exists, then the default implementation of T'Put_Image is the same as described below for a nonderived record type.

@dby

For a nonnull type extension, or an untagged derived type declared with a @fa{known_discriminant_part}, the default implementation of T'Put_Image depends on whether there exists a noninterface ancestor of T (other than T itself) for which the Put_Image aspect has been directly specified. If so, then:

@xbullet{if T is a nonnull type extension, T'Put_Image will generate an image based on extension aggregate syntax where the ancestor type of the extension aggregate is the nearest ancestor type whose Put_Image aspect has been specified;}

@xbullet{if T is an untagged derived type with a @fa{known_discriminant_part}, T'Put_Image invokes the Put_Image for its parent type on a conversion of the parameter of type T to the parent type.}

If no such ancestor exists, then the default implementation of T'Put_Image is the same as described below for a nonderived record or protected type.

!corrigendum 4.10(21/5)

@drepl

For a record type (or, as indicated above, a type extension with no noninterface ancestor specifying Put_Image), or a protected type, the default implementation of T'Put_Image generates an image based on named (not positional) record aggregate syntax (except that for a protected type, the initial left parenthesis is followed by @fc{“PROTECTED with “}). Component names are displayed in upper case, following the rules for the image of an enumeration value. Component values are displayed via calls to the component type's Put_Image procedure.

@dby

For a record type (or, as indicated above, certain composite derived types), or a protected type, the default implementation of T'Put_Image generates an image based on named (not positional) record aggregate syntax (except that for a protected type, the initial left parenthesis is followed by @fc{“PROTECTED with “}). Component names are displayed in upper case, following the rules for the image of an enumeration value. Component values are displayed via calls to the Put_Image procedure of the component type.

!ACATS test

Existing Put_Image ACATS tests should be adjusted as appropriate, and a new test should be written to check the case specifically of an untagged derived type with a new set of discriminants, with new names and/or with fewer discriminants.

!appendix

!topic Components of derived untagged discriminated record types

!reference Ada 2022 RM3.7(18-25), RM4.10(7/5,17/5,18/5), RM13.13.2(8.2/2-9.1/5)

!from Christoph Grein 2023-12-22

!keywords discriminants, record components, derived types

!discussion

First of all: Merry Christmas to all of you and Happy New Year

Which record components of One_Horse_Carriage do exist? Code see at end. This is my attempt of an RM exegesis.

RM 3.7(18) The discriminant Number_Passengers of the parent type Carriage is constrained to equal the new discriminant of the derived type One_Horse_Carriage; they correspond (and may share storage).

The discriminant Number_Horses of the parent type Carriage is constrained to the value of the expression 1; it is specified.

(19) The component_definition Persons depends on the discriminant Number_Passengers.

The component_definition Horses does not depend on a discriminant.

(23) The component Passengers depends on the discriminant Number_Passengers.

The component Team does not depend on a discriminant.

(25) Each value of a discriminated type includes a value for each component of the type that does not depend on a discriminant.

Thus, Number_Passengers, Coachman and Team are included.

I'm tempted to say, also the component Number_Horses is included with the fixed value 1 - it does not depend on a discriminant.

The values of discriminants determine which other component values (only Passengers here) are present in the value of the discriminated type. Where RMss.ss(pp)? According to 3.7.1(11), I would expect something like: Each component whose constraint is satisfied.

Obviously, I'm misreading the RM, since Number_Horses allegedly is not included, no value may be given in the aggregate. Barnes's book Ada 2012 also says this in a different example (Boxer) on chapter 18.4.

I'm really astonished that the new attribute 'Image for a record includes a value for a nonexistent component.

Image attribute

===============

RM 4.10(7/5) For an untagged derived type, ..., the default implementation of T'Put_Image invokes the Put_Image for its parent type on a conversion of the parameter of type T to the parent type.

Does this make sense if the parent type has more components than the derived type?

(17/5) For a specific, nonderived composite type:

(18/5) If the default implementation of Put_Image writes components, the order in which components are written is the same canonical order in which components of a composite type T are written out by the default implementation of T'Write. [This is also the order that is used in determining the meaning of a positional aggregate of type T.]

Stream attributes

=================

RM 13.13.2(8.2/2) The default implementations of the Write and Read attributes, where available, execute as follows:

(9/5) ... For nonderived composite types, the Write or Read attribute for each component (excluding those, if any, that are not components of the nominal type of the object) is called in ... positional aggregate order for a record. ... If T is a discriminated type, discriminants are included only if they have defaults. ...

This is funny - discriminants are output although there are no defaults.

(9.1/5) ... For untagged derived types, the Write (resp. Read) attribute invokes the corresponding attribute of the parent type, if the attribute is available for the parent type.

This last sentence does not make sense for cases like Sulky where a certain discriminant is no longer part of the record.

I admit, a lot of ranting. And GNAT has perhaps some bugs (Ada 2022 is not yet fully implemented as you can see because it does not accept [] for array aggregates, but uses them for output).

This is my sample program:

---------------------------

with Ada.Text_IO;use  Ada.Text_IO;

procedure Coaches is

  subtype Number is Natural range 0 .. 8;

  subtype Person  is Positive;

  type        Persons is array (Number range <>) of Person;

  subtype Horse  is Positive;

  type        Horses is array (Number range <>) of Horse;

  type Carriage (Number_Passengers, Number_Horses: Number) is record

        Coachman  : Person;

        Passengers: Persons (1 .. Number_Passengers);

        Team          : Horses  (1 .. Number_Horses);

  end record;

  type One_Horse_Carriage (Number_Passengers: Number) is

        new Carriage (Number_Passengers => Number_Passengers,

                    Number_Horses => 1);

  Sulky: One_Horse_Carriage (Number_Passengers => 0) :=

        (Number_Passengers => 0,

         Coachman              => 1850,

         Passengers            => (-5 .. -6 => 1),  -- []  Ada 2022

         Team                  => (1 => 2023));

  Coach          : Carriage               := Carriage (Sulky);

                     -- Does type conversion work ...

  Sulky_again: One_Horse_Carriage := One_Horse_Carriage (Coach);

                  -- ... in both directions?

begin

  Put_Line ("Sulky");

  Put_Line ("=====");

  Put_Line (Sulky.Number_Passengers'Image);

  --Put_Line (Sulky.Number_Horses'Image);  -- does allegedly not exist, yet ...

  Put_Line (Sulky.Coachman'Image);

  Put_Line (Sulky.Passengers'Image);

  Put_Line (Sulky.Team'Image);

  Put_Line (Sulky'Image);                      -- GNAT prints it out here

  New_Line;

  Put_Line ("Coach");

  Put_Line ("=====");

  Put_Line (Coach.Number_Passengers'Image & Coach.Number_Horses'Image);

  Put_Line (Coach.Coachman'Image);

  Put_Line (Coach.Passengers'Image);

  Put_Line (Coach.Team'Image);

  Put_Line (Coach'Image);

  New_Line;

  Put_Line ("Sulky = Sulky_again ?");

  Put_Line ("=====================");

  Put_Line (Boolean'Image (Sulky = Sulky_again));

end Coaches;

-------------------------------------------------

This is the result:

-------------------------------------------------

Sulky

=====

 0

 1850

[]

[ 2023]

(NUMBER_PASSENGERS =>  0,

 NUMBER_HORSES =>  1,

 COACHMAN =>  1850,

 PASSENGERS =>

 [],

 TEAM =>

 [ 2023])

Coach

=====

 0 1

 1850

[]

[ 2023]

(NUMBER_PASSENGERS =>  0,

 NUMBER_HORSES =>  1,

 COACHMAN =>  1850,

 PASSENGERS =>

 [],

 TEAM =>

 [ 2023])

Sulky = Sulky_again ?

=====================

TRUE

-----------------------------------------

From: Randy Brukardt

Sent: Saturday, December 23, 2023  1:01 AM

>        First of all: Merry Christmas to all of you and Happy New Year

Are you sure you sent this the right time of year? It seems more like an April Fool's note to me. But Merry Christmas to you as well, just don't expect a lengthy and detailed answer for a while.

>        Which record components of One_Horse_Carriage do exist? Code see at end.

First, was this something that was encountered in actual use, or is it more of a language lawyer thought experiment. It's pretty clear that changing discriminants on untagged derived types is one of those features that keep language lawyers employed; they tend to break many otherwise good ideas.

Moreover, they seem to be largely useless. Indeed, until Ada 2022 one couldn't even have a different representation for a derived type, so  any untagged derivation was close to useless (the main usage has been to rename a type into a different scope, and they're not ideal for that). Changing discriminants are not necessary for that primary use, so actual usage is next to none. I'd guess that totally removing the feature from Ada would affect essentially no one except those that run the ACATS. (I don't recall ever having a bug report on this feature in Janus/Ada, even though it is known not to work in a number of corner cases.)

As to whether the discriminants exist, on what basis:

   (1) Logically in the user view: They don't exist, thus they don't appear in aggregates.

   (2) To an implementation: Of course all of the discriminants exist (certainly in the absence of a representation clause). The record representation has to be the same for many derived record types (for instance, consider if the parent type was a by-reference type), otherwise conversions would be impossible. And certainly they exist for any fully constrained subtype, so it would be bizarre to try to do without them only in the weird case of a partially constrained derived type. It's too much work to implement such a type in the absence of anyone caring beyond passing ACATS tests.

>... [Image attribute] ... Does this make sense if the parent type has

>more components than the derived type?

Well, most of the time, yes. If the parent type has a user-defined Put_Image, we certainly want to use it. And assuming the derived type has the same representation (it certainly will in this example), it makes sense to use the same Image. I don't think we ever promised that the Image was something that could be used directly as an aggregate.

---

I'm not going to spend any more time on this now. Christmas, you know. ;-) I'm not entirely sure what question you are actually trying to ask. I'd argue that the only important thing about this feature is that it is defined in each case such that there is no language hole. Beyond that, no one uses this so no one will ever care how it is defined.

I don't think it is possible to consistently define this feature (since it depends on the rather nasty idea of hiding components that still exist), and I don't know if it is worth anyone's time to try.

How do YOU want this to work?? Explain your answer.


 

From: Tucker Taft

Sent: Tuesday, December 26, 2023  1:45 PM

I agree the current rules for the default for Put_Image of an untagged derived type do not properly handle the case when the discriminants are replaced.  It should call the parent’s Put_Image only if it has been specified on the parent or some more distant ancestor. Otherwise it should default to a component by component construction of the image, given that the number and names of the discriminants might have changed.


 

From: Christoph Grein

Sent: Wednesday, February 14, 2024  5:35 AM

first of all, I have to apologize for pestering you with this mail. It was never meant to be sent. I wrote it with the intention to send it and collected the RM paragraphs, but the more I did I saw that I wasn't ready. So I wanted to quit and pressed the wrong button.

I thought about immediately sending a second mail saying "please ignore" - but then: what I writ I writ!

So I made a long pause before I now come back to this. Apology again for answering so late. And thank you for your patience.

Randy: How do YOU want this to work?? Explain your answer.

In effect, I had expected something like what GNAT did, a kind of aggregate, but without the discriminants that do not occur in an aggregate. So I tried to understand what the RM has to say... (not a moderate task)

I've read the complete AIs about the new Image. (There is a remark that perhaps later a matching Value could be defined. I guess this idea has been dropped. How could this be done with Image having components not available in the aggregate.)

I read RM 4.10(7/5) that the parent image should be given, which GNAT did not do (CE 2021; perhaps this has changed meanwhile) because it used the actual discriminant names. [This is not a bug report.]

I see there are conflicting aims: Using extension aggregates when Put_Image is defined for an ancestor (which might have different discriminants), else using the actual names. I gather that Tuck feels some sympathy for using actual names!

So in effect the current solution for untagged records looks OK (replacing discrimininants instead of inheriting them seems like nonsense).

What I do not understand is RM 4.10(17/5, 18/5) talking about the default implementation of T'Write. T'Write does not include discriminants except when they have defaults, which they do not have here (yet GNAT outputs them). An image without discriminants seems like utter nonsense to me - which is what GNAT creates for Image with tagged derived types under certain conditions! Is this really the intention? See below:

-----------------------

with Ada.Text_IO;

use  Ada.Text_IO;

with Interfaces;

use  Interfaces;

procedure Sequence is

  type T0 (D0: Integer_8) is tagged record

        K0: Integer_8;

  end record;

  type T1 is new T0 (D0 => -1) with record

        K1: Integer_8;

  end record;

  procedure P0 is

        --            D0  K0

        X: T0 := ( 1, -1);

   begin

        Put_Line ("P0");

        Put_Line ("==");

        Put_Line (X'Image);

        New_Line;

  end P0;

  procedure P1 is

        --            D0  K0 K1

        X: T1 := (-1, -2, 8);  -- D0 must be -1

  begin

        Put_Line ("P1");

        Put_Line ("==");

        Put_Line (X'Image);

        New_Line;

  end P1;

begin

  P0;

  P1;

end Sequence;

-----------------------

P0

==

(D0 =>  1,

 K0 => -1)

P1

==

(K0 => -2,

 K1 =>  8)


 

From: Tucker Taft

Sent: Wednesday, February 14, 2024  10:11 AM

As I indicated, I agree with your concern.  I have created a new issue on the ARG GitHub site:

   https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues/85

Please take a look to verify that I have captured your concern.

In the future I would encourage you to use the ARG GitHub site instead of ada-comment, as the GitHub site provides a more open place to discuss language issues.  You can look at all of the existing issues, and open new ones at:

  https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues


 

From: Randy Brukardt

Sent: Wednesday, February 14, 2024  10:27 PM

> So I made a long pause before I now come back to this. Apology again

> for answering so late. And thank you for your patience.

The one thing you didn't answer is why you were thinking about this case in the first place. I'm still curious as to what use case led you to thinking about disappearing discriminants, which seem altogether useless to me.

The usual use of untagged derivation is to stand-in for the missing type renames, and no one needs to change any discriminants for that. Otherwise, derivation is pretty useless, especially in pre-Ada 2022 when representation changes are mostly impossible. So even encountering this feature requires using a rather useless larger feature.

I have a hard time getting excited about spending hours thinking about and then arguing about the semantics of something that is never going to occur in the first place. Thus the original question about actual use as opposed to a thought experiment.

P.S. I sent this on Ada-Comment as the OP is clearly more comfortable here, and this question is more to satisfy my curiosity than anything about the actual language issue.


 

From: Christoph Grein

Sent: Saturday, February 17, 2024  7:06 AM

> The one thing you didn't answer is why you were thinking about this

> case in the first place. I'm still curious as to what use case led you

> to thinking about disappearing discriminants, which seem altogether useless to me.

You're right, this is more of an academic discussion. After retirement, I held some basic Ada programming courses in German, which I finally made public (in very extended form because for self study, there is no time limit like in a payed course constrained to a few days) at Ada Germany web site.

I'm now working on a future public course about OOP, and I sought an application for untagged discriminated types. I myself do not find the Coaches example very convincing. But the RM requirements are not convincing either :-(

I'll continue the discussion on GitHub.


 

From: John Barnes

Sent: Saturday, February 17, 2024  4:37 PM

There are several examples of untagged discriminated types in my book. Many of them have been there since I first wrote it in 1981.


 

From: Jeffrey R. Carter

Sent: Monday, February 19, 2024  5:07 PM

Yes, there are many uses for discriminated record types. The PragmAda Reusable Components (https://github.com/jrcarter/PragmARC) define 12 of them.


 

From: Randy Brukardt

Sent: Tuesday, February 20, 2024  1:43 AM

> Yes, there are many uses for discriminated record types.

Surely. But I think his question was not about untagged discriminated record types per-se, but rather about derived untagged discriminated record types with changed discriminants. A construct which is *much* less useful, and is almost certainly not useful enough to justify the substantial definitional and implementation pain associated with it.


 

From: Christoph Grein

Sent: Tuesday, February 20, 2024  4:00 AM

> But I think his question was not about untagged discriminated record types per-se, but rather about derived untagged discriminated record types with changed discriminants.

Exactly. Nobody questioned the usefulness of untagged discriminated types.

I would like to see a convincing example for changing the names of discriminants of untagged types. My coaches example isn't it nor the matrix and square matrix example RM 3.7(34-35). (Ellipse and circle don't count either.)


 

From: Tucker Taft

Sent: Tuesday, February 20, 2024  6:50 AM

Be that as it may, it is way too late to decide that this feature should be ignored when it comes to an appropriate 'Image result.


 

From: Jeffrey R. Carter

Sent: Tuesday, February 20, 2024  5:29 AM

There are also uses for derived record types, but I agree that I've never had a use for derivation with changed discriminants.


 

From: Randy Brukardt

Sent: Wednesday, February 21, 2024  9:38 PM

>Please move this discussion to the ARG GitHub issue.

I put this discussion here because it is more of a gripe session than anything actionable, and I didn't want to pollute the actual problem with it.

I think it is important to realize that this is a completely useless feature, but I do agree that that doesn't give us license to have it broken in some way.


 

This issue was moved to and documented in ARG GitHub issue # 85.


 

Editor’s note, November 4, 2024: I changed 4.10(21/5) to eliminate the possessive “component type’s”. That is a pet peeve of John Barnes, and it seems highly likely that he would complain about it in Editorial Review. So I’m just saving myself some work by fixing it now. Consider this my Editorial Review.