!standard 4.3.4(14/5) 20-06-10 AI12-0381-1/02
!class Amendment 20-06-09
!status work item 20-06-09
!status received 20-04-19
!priority Low
!difficulty Easy
!subject Tag of a delta aggregate
!summary
The tag of a delta aggregate of a specific tagged type is that of the type.
!problem
Consider:
type Some_Tagged is tagged record
L : Natural;
Data : array (1..10) of Some_Controlled_Type;
end record;
procedure Do_Something (A : Some_Tagged) is
My_Obj : Some_Tagged := (A with delta L => 10); -- (1)
begin
...
end Do_Something;
type An_Ext is new Some_Tagged with record
Z : Some_Controlled_Type;
end record;
Obj : An_Ext := ...;
Do_Something (Some_Tagged (Obj));
Some_Tagged is required to be built-in-place by 7.6(17.3/3). The tag of the
aggregate (1) is that of An_Ext by 4.3.4(14/5), as the base expression has
that tag (type conversions do not change the tag of tagged objects).
The base expression is used to initialize My_Obj, which must include copying
and adjusting the extension component Z. But there is no place in My_Obj for
Z -- My_Obj does not have that extension.
If the array of Some_Tagged had been of some non-controlled type (say Natural),
then we would still be required to build the aggregate in place (as there
still is a controlled component in the extension), but the compiler doesn't
even know that at compile-time for the aggregate at (1). Clearly something is
wrong here.
!proposal
We propose to eliminate the rule that the tag of the new object comes from
the base object, replacing that with the type of the aggregate. We also
adjust the wording so it is clear that the aggregate is initialized with
the part of the base_expression that corresponds to that tag.
Most other cases in Ada discard the tag of the original value; even a function
return discards the tag of the returned expression, so it seems confusing that
a delta aggregate of a specific type would try to preserve the underlying tag
of the base object. If that is the desired semantics, the base expression can
be converted to a class-wide type, to make that intent very clear.
!wording
Modify 4.3.4(14/5):
The evaluation of a delta_aggregate begins with the evaluation of the
base_expression of the delta_aggregate; then that value is used to create
and initialize the anonymous object of the aggregate. The bounds of the
anonymous object of an array_delta_aggregate and the discriminants [and
tag] (if any) of the anonymous object of a record_delta_aggregate are
those of the base_expression. {If a record_delta_aggregate is of a
specific tagged type, its tag is that of the specific type; if it is of
a class-wide type, its tag is that of the base object.}
Replace 4.3.4(14.b/5) [part of an AARM Rationale] with:
For a specific tagged type, any extension components that the actual object
underlying the base_expression might have are not used to initialize the
anonymous object, which is critical if the aggregate is required to be
built-in-place.
!discussion
The alternative to the proposed solution would be some sort of modification
to the build-in-place rules that would avoid copying (unknown) extension
components. This would mean that the semantics would be subtly different
when built-in-place is used, as some Adjust/Finalize pairs that otherwise
would be called would not be called. This seems unwise, especially as
implementations are allowed but not required to use build-in-place in many
additional cases.
Moreover, the original requirement to use the tag of the base object would
mean that for the case when build-in-place is not used, the size of the
aggregate would depend on the base object. For many cases, this is not
known at compile-time. This means that shifting from an untagged record to a
tagged record will require switching from a static to dynamic allocation
of temporary space with the resulting increase in implementation complexity
and expense. It might also be necessary to adjust the result even though
no controlled components are visible.
There is no other operation in the language that operates this way other than
'Old, and 'Old is only allowed within postconditions, which are not considered
performance sensitive. Furthermore, 'Old preserves everything, whereas a delta
aggregate only allows specifying changes to the visible components, with no
way to indicate a change in the parts that are in whatever unknown extension
the object might have. This means that if the base object were passed via a
redispatch to some operation that could see the extension, it must not
actually make any change to the extension, and only would be allowed to change
the part visible originally. This seems to be a real corner case, and not
something justifying this added expense, and quite possibly misleading in
cases where in some overridings of the operation parts of the extension *are*
updated, and in others not. Note also that any assignment or comparison
against the delta aggregate would ignore the tag and the extension parts,
which makes all the work seem doubly wasteful.
We conclude that the base expression should be "truncated" to the type of the
aggregate, if that type is a specific tagged type.
Note that we do allow delta aggregates to have class-wide tagged types. This
provides all of the capability that might be lost by changing the
specific-type delta aggregate, and makes the semantics very clear and
explicit.
!examples
X : T := (Y with delta A => 3, B => 4);
-- Size and tag of aggregate are determined by T.
Above delta aggregate is roughly equivalent to (ignoring the liberties with the
syntax):
Temp : constant T := Y;
Temp.A := 3;
Temp.B := 4;
X : T := Temp;
If the build-in-place permission is used, the Temp can be eliminated, giving:
X : T := Temp;
X.A := 3;
X.B := 4;
!ASIS
No ASIS effect.
!ACATS test
An ACATS C-Test is needed to check that the new rules are enforced,
rather than the previous rules. The test program given below can be the
basis of an ACATS C-Test.
!appendix
From: Tucker Taft
Sent: Sunday, April 19, 2020 7:07 AM
I just bumped into a case where I really wanted to change an access discriminant
of a record object, and nothing else. So I immediately thought how nice it
would be to have delta aggregates. And then to my dismay realized that we might
not allow this use case, if we have assignment semantics of (record) delta
aggregates. So I looked at the proposed wording in 4.3.4 and sure enough we use
(re)assignment rather than initialization semantics for record delta aggregate.
I realize that for array delta aggregates we get some value out of using
(re)assignment semantics, but I am not convinced that we do for record delta
aggregates, and we clearly lose the ability to substitute a new discriminant
value.
Here is the existing Dynamic Semantics wording from 4.3.4:
14/5
{AI12-0127-1} {AI12-0324-1} The evaluation of a delta_aggregate begins with the
evaluation of the base_expression of the delta_aggregate; then that value is
used to create and initialize the anonymous object of the aggregate. The bounds
of the anonymous object of an array_delta_aggregate and the discriminants and
tag (if any) of the anonymous object of a record_delta_aggregate are those of
the base_expression.
14.a/5
Ramification: This is the same anonymous object as described in 7.6, “Assignment
and Finalization”; in particular, it might be required to be built in place.
14.b/5
This requires that the underlying tag (if any) associated with the
delta_aggregate is that of the base_expression in the delta_aggregate and not
that of the nominal type of the base_expression in the delta_aggregate.
14.c/5
To be honest: The anonymous object associated with the evaluation of a
delta_aggregate begins its life as a variable, not a constant (3.3
notwithstanding). This must be the case because the object is initialized and
then subsequently modified. After evaluation of the delta_aggregate is complete,
the object is a constant object. This is similar to the way that an extended
return statement can provide a variable view of an object that will eventually
be a constant object after the function returns its result.
15/5
{AI12-0127-1} For a record_delta_aggregate, for each component associated with
each record_component_association (in an unspecified order):
16/5
* if the associated component belongs to a variant, a check is made that the
values of the discriminants are such that the anonymous object has this
component. The exception Constraint_Error is raised if this check fails.
17/5
* the expression of the record_component_association is evaluated, converted to
the nominal subtype of the associated component, and assigned to the
component of the anonymous object.
----
I would propose that we split the dynamic semantics for record delta aggregates
and array delta aggregates completely. We already did that for the Name
Resolution and Legality Rules. For record delta aggregates, I think we could
make the semantics be more like creating a new aggregate, rather than like
copying (with adjust, presumably) and then assigning components.
The wording in 4.3.1 in the Name Resolution and Legality Rules sections would
have to allow discriminants to be named in a record delta aggregate
component_selector_name. A separate Legality Rule would disallow naming a
discriminant that governs a variant part of the record type.
The dynamic semantics of 4.3.4 would end up something like this:
The evaluation of a record_delta_aggregate begins with the evaluation of the
base_expression of the record_delta_aggregate. Then, for each component
associated with each record_component_association:
* if the associated component belongs to a variant, a check is made that the
values of the discriminants of the base object are such that the anonymous
object has this component. The exception Constraint_Error is raised if this
check fails.
* the expression of the record_component_association is evaluated, converted to
the nominal subtype of the associated component, and defines the value of the
associated component of the anonymous object; if the associated component is
a discriminant, any per-object constraint that depends on the discriminant is
then elaborated.
The above evaluations (and conversions) happen in an arbitrary order, except
that the evaluation and conversion of the expression for a component that
depends on a discriminant specified in the record_component_association_list
happens after the elaboration of any per-object constraint that depends on that
discriminant.
The value of any component of the anonymous object that is not associated with
one of the record_component_associations is defined by the corresponding
component of the result of evaluating the base_expression.
---
Comments?
****************************************************************
From: Richard Wai
Sent: Sunday, April 19, 2020 7:20 PM
> I just bumped into a case where I really wanted to change an access
> discriminant of a record object, and nothing else. So I immediately
> thought how nice it would be to have delta aggregates. And then to my
> dismay realized that we might not allow this use case, if we have
> assignment semantics of (record) delta aggregates. So I looked at the
> proposed wording in 4.3.4 and sure enough we use (re)assignment rather
> than initialization semantics for record delta aggregate.
This seems like an extremely useful feature which would definitely be a natural
application of record delta aggregates!
> I realize that for array delta aggregates we get some value out of
> using (re)assignment semantics, but I am not convinced that we do for
> record delta aggregates, and we clearly lose the ability to substitute
> a new discriminant value.
This makes sense. Particularly it seems counter-intuitive that the
base_expression is responsible for "initializing" the anonymous object since a
delta aggregate is an obvious way to generate a new record based on an existing
"progenitor" / "template". In my experience, this is not unusual at all, and
currently entails a needlessly verbose aggregate with a lot of (A => X.A, B =>
X.B), and isn't that the whole point of having delta aggregates in the first
place?
In other words, initialization of new object via a delta aggregate would
probably be common, and in many of those cases you'd naturally want to be
setting the discriminants as well.. And if it was legal to do that with a
regular aggregate, it shouldn't be illegal for a delta aggregate!
> ----
>
> I would propose that we split the dynamic semantics for record delta
> aggregates and array delta aggregates completely. We already did that
> for the Name Resolution and Legality Rules. For record delta
> aggregates, I think we could make the semantics be more like creating
> a new aggregate, rather than like copying (with adjust, presumably) and then assigning components.
This definitely is nice an orthogonal, and I can't imagine any good argument
against it (though with my limited ARG background, that probably doesn't say
much).
> The dynamic semantics of 4.3.4 would end up something like this:
>
But in many these semantics seem even clearer than the original (to me).
> The above evaluations (and conversions) happen in an arbitrary order,
> except that the evaluation and conversion of the expression for a
> component that depends on a discriminant specified in the
> record_component_association_list happens after the elaboration of any
> per-object constraint that depends on that discriminant.
>
I suppose this might be the tricky part..
> ---
>
> Comments?
Strong support from my end.
****************************************************************
From: Tucker Taft
Sent: Sunday, April 19, 2020 7:56 PM
Thanks, Richard. Glad it seems useful to you. I realize that one fix needed is
to revise the final sentence as follows:
The value of any component of the anonymous object that is not associated with
one of the record_component_associations is defined by the corresponding
component of the result of evaluating the base_expression{, converted to the
subtype of the component as determined by the new discriminant values}.
This conversion is really just a constraint check, though it could be doing
sliding if both the low and high bounds were controlled by updated
discriminants. Normally you would only update a discriminant if you intended to
update the discriminant-dependent components as well.
****************************************************************
From: Tucker Taft
Sent: Sunday, April 19, 2020 8:29 PM
I suppose we could go one step further, and allow updating discriminants that
govern a discriminant part, so long as all of the newly "needed" components are
provided in the record_component_associations of the delta aggregate. I thought
perhaps that was not particularly useful, but in fact it might be if the record
has a lot of components in the non-variant part, and only a few components in
the variant part. Once you see the base_expression as a kind of "backup" that
provides the needed but unspecified components, changing a discriminant is not
that big of a deal.
****************************************************************
From: Jean-Pierre Rosen
Sent: Monday, April 20, 2020 3:27 AM
>> I realize that for array delta aggregates we get some value out of
>> using (re)assignment semantics, but I am not convinced that we do for
>> record delta aggregates, and we clearly lose the ability to
>> substitute a new discriminant value.
> This makes sense. Particularly it seems counter-intuitive that the
> base_expression is responsible for "initializing" the anonymous object
> since a delta aggregate is an obvious way to generate a new record
> based on an existing "progenitor" / "template". In my experience, this
> is not unusual at all, and currently entails a needlessly verbose
> aggregate with a lot of (A => X.A, B => X.B), and isn't that the whole
> point of having delta aggregates in the first place?
Which makes me think (admitedly without studying all the implications), why
don't we simply use the model of default values of subprograms, rather than that
bunch of complicated rules?
i.e., in (X with delta ...)
every needed component (including discriminants) of the aggregate not
explicitely provided is added by the compiler as C => X.C, and then let normal
aggregates rules work. Might be slightly more difficult for array aggregates
though.
****************************************************************
From: Tucker Taft
Sent: Monday, April 20, 2020 6:34 AM
Array delta aggregates are intentionally different, because they allow multiple
dynamic replacements, which might overlap, so it is defined to be an ordered
sequence. Record delta aggregates are unordered; positional notation is in fact
not permitted. I agree that the model of defaults is a nice one, but as usual
with wording, the devil is in the details.
****************************************************************
From: Randy Brukardt
Sent: Monday, April 20, 2020 1:04 PM
That model only works when the set of components is known at compile-time. For a
delta aggregate that includes no discriminants when the base expression is
unconstrained and (especially) has variants, there's no way to figure out the
components needed at compile-time. And that would require a completely new
mechanism unlike anything currently in Ada. Given that the initial use case for
delta aggregates is exactly this sort of record type, that seems very bad.
****************************************************************
From: Gary Dismukes
Sent: Monday, April 20, 2020 1:21 PM
> ...
> ----
>
> I would propose that we split the dynamic semantics for record delta
> aggregates and array delta aggregates completely. We already did that
> for the Name Resolution and Legality Rules. For record delta aggregates,
> I think we could make the semantics be more like creating a new aggregate,
> rather than like copying (with adjust, presumably) and then assigning
> components.
>
> The wording in 4.3.1 in the Name Resolution and Legality Rules sections
> would have to allow discriminants to be named in a record delta aggregate
> component_selector_name. A separate Legality Rule would disallow naming a
> discriminant that governs a variant part of the record type.
>
> The dynamic semantics of 4.3.4 would end up something like this:
>
> ...
>
> Comments?
Sounds to me like a useful and relatively straightforward change.
****************************************************************
From: Randy Brukardt
Sent: Monday, April 20, 2020 1:26 PM
I'm opposed to this change for two reasons.
First, I think that allowing general delta aggregates essentially eliminates
much of the value of aggregates in the first place, which is the requirement
that all components be covered. By definition, delta aggregates do not do that.
Having such aggregates that change a component or two doesn't really harm that
value much. But once we start allowing completely arbitrary changes, that
protection is completely gone. Anyone could skip the completeness check by using
a delta aggregate on a dummy object.
One of the reasons that I use Ada is because it protects me from my worst
instincts. I'm not happy with giving me another way to end-run protection,
because I *know* I'll end up using it.
I realize that Tucker proposed not allowing variant changes in delta aggregates.
That essentially means that I personally could never use the enhanced
possibilities (the only discriminants I use control variants). So this would be
a lot of work for me with no personal benefit.
And this is a massive amount of work for Janus/Ada. The existing semantics of
delta aggregates is described in terms of operations that already exist
(initializing an object from another object, and writing an aggregate component
into an object). However, if any discriminant controlling a component can be
changed by an aggregate, then that is no longer true. That's because
initializing a temporary from the base object would no longer work -- it would
allocate possibly the wrong size for discriminant-dependent components. The only
way to implemnt this would be to copy the discriminants from be base object into
the top-level temporary (and a temporary would be required somewhere), set the
discriminants from the aggregate, allocate the memory for the temporary object,
and then copy the other components either from the aggregate or the base object
individually. That would be 100% new code (there's nothing like that anywhere in
Ada now). It could be done without any massive changes sewhere only if variants
are not allowed (as Tucker had proposed), but it would be a lot or work for very
little gain.
I also note that this would introduce a number of new cases for coextensions
(that is, a useless concept layered on top of a rarely used concept ;-). In
particular, you could change whether each the discriminants individually is a
coextension. (One could be changed and one could be left the same; currently,
you can only change them all at once.) Not sure if this would make things harder
(this is a feature not close to worth the work, so I haven't analyzed it very
carefully).
Anyway, I don't think any of these issues is likely to convince many others, so
if the rest of you want to go ahead, don't let me stop you. But don't expect any
support from me, either.
****************************************************************
From: Steve Baird
Sent: Monday, April 20, 2020 1:48 PM
> i.e., in (X with delta ...)
> every needed component (including discriminants) of the aggregate not
> explicitely provided is added by the compiler as C => X.C, and then
> let normal aggregates rules work.
Randy pointed out one case where the set of needed components cannot be
identified statically. Here is another one, based on the fact that a delta
aggregate (X with delta ...) preserves the underlying tag of X in the case where
X is tagged.
IIUC, the proposed rule would not properly initialize the F3 component of the
delta aggregate in this example:
procedure Delta_Agg is
type T1 is tagged record
F1, F2 : Integer;
end record;
type T2 is new T1 with record
F3 : Integer;
end record;
procedure P (X : T1) is
Y : T1'Class := (X with delta F2 => 444);
begin
pragma Assert (Y = T1'Class (T2'(111, 444, 333)));
end P;
begin
P (T1 (T2'(111, 222, 333)));
end Delta_Agg;
I'm not saying this couldn't be fixed, but a fix of some sort would be needed.
****************************************************************
From: Randy Brukardt
Sent: Monday, April 20, 2020 4:04 PM
Thanks, Steve. You've pointed out that since one can have a delta aggregate for
a class-wide type (or a specific type where the actual object is of some
descendant), you don't know at compile-time even what components *might* need to
be copied. (For an untagged type, you at least know the components that might be
involved, although with no idea which ones exactly are involved.)
So that means that one would have to, at a minimum, have different
implementations for tagged and untagged types (note that one can derived a
mutable untagged record from an immutable untagged record, so that in general
one cannot assume anything because the type you are holding is immutable,
something I ran into in Janus/Ada debugging over the weekend). A tagged delta
aggregate cannot change any discriminants, so including them would be noise
(which would have to be checked at runtime as well). And the very messy
implementation I outlined before would be needed otherwise.
Moreover, a component-at-a-time model like described by J-P would be nearly
impossible to implement for a tagged object, as the underlying object could be
any extension of it, and copying a bunch of unknown components seems essentially
impossible. One couldn't reasonably use a dispatching operation to do that,
since one would need such an operation for every combination of types and
specified components possible (which is obviously impractical). Even one such
operation for every ancestor type seems impractical.
I conclude that a "component-at-a-time" model doesn't work,, both from a
language definition point of view, and from an implementation point of view. The
model Tucker suggested sort of works, but it requires changing the "shape" of
temporary objects after they're created, which is a new requirement on Ada
implementations. That also seems like a bad idea to me.
****************************************************************
From: Tucker Taft
Sent: Monday, April 20, 2020 7:26 PM
OK, thanks for all the useful feedback. Let me try to write up something that
steers clear of these icebergs, but still provides some useful value. I think
it is possible, but we'll see! If all else fails we fall back to the existing
capability, which is still useful. But I think it will be frustrating in some
cases when a "simple" discriminant update is desired. But I can see that going
for full generality has lots of problems. So my intent is to find an
intermediate position that supports the common cases, while avoiding the nasty
corners.
****************************************************************
From: Randy Brukardt
Sent: Monday, April 20, 2020 7:47 PM
I think you *might* be OK if you restrict yourself to discriminants that don't
discriminate anything (that is, have no discriminant-dependent components), and
probably require all or nothing on access discriminants (not as sure about
this). Your original case of changing an access discriminant is usually in that
category.
****************************************************************
From: Jean-Pierre Rosen
Sent: Tuesday, April 21, 2020 2:29 AM
> First, I think that allowing general delta aggregates essentially
> eliminates much of the value of aggregates in the first place, which
> is the requirement that all components be covered. By definition,
> delta aggregates do not do that. Having such aggregates that change a
> component or two doesn't really harm that value much. But once we
> start allowing completely arbitrary changes, that protection is
> completely gone. Anyone could skip the completeness check by using a
> delta aggregate on a dummy object.
I must say I have some sympathy with Randy's position. TBH, I am worried that
the trend of evolution is for ease of writing more than ease of reading or
maintaining.
That being said, if the "default value" model does not work, it seems to me that
a delta aggregate: (Rec with delta A=> Val_A, B => Val_B)
should be equivalent to:
declare
Temp : Rec_T := Rec;
begin
Temp.A := Val_A;
Temp.B := Val_B;
-- use Temp...
end;
Of course, this model may imply more adjust/finalize than the current proposal
if there are controlled components. It also shows that delta aggregates are, at
best, a convenience.
OTOH, I understand that nowadays, ease of writing makes more for
popularity of languages than ease of maintenance. That's another one of my
concerns...
****************************************************************
From: Randy Brukardt
Sent: Tuesday, April 21, 2020 5:35 PM
> That being said, if the "default value" model does not work, it seems
> to me that a delta aggregate:
> (Rec with delta A=> Val_A, B => Val_B)
>
> should be equivalent to:
> declare
> Temp : Rec_T := Rec;
> begin
> Temp.A := Val_A;
> Temp.B := Val_B;
> -- use Temp...
> end;
This clearly does not allow changing any discriminants (assiging a discriminant
individually is never allowed and would be nonsense if there are any
discriminant-dependent components). If we stick with that, we have an adequate
model for delta aggregates. (Which I believe is essentially the above.) The
problems come if we want to allow changing discriminants as Tucker proposed.
****************************************************************
From: Richard Wai
Sent: Wednesday, April 22, 2020 9:26 PM
> I must say I have some sympathy with Randy's position. TBH, I am
> worried that the trend of evolution is for ease of writing more than
> ease of reading or maintaining.
> OTOH, I understand that nowadays, ease of writing makes more for
> popularity of languages than ease of maintenance. That's another one
> of my concerns...
>
I strongly agree with you philosophically (a moment of silence of '@').. Though
it seems the initial motive behind delta aggregates was to heavily weighted
towards making post conditions more concise, and that remains very valuable from
a readability standpoint. I suppose it has grown into a whole other beast when
you start talking about more general use cases.
To me, Tucker's proposal adds some general elegance and regularity (from a user
perspective) to record delta aggregates - a sort of universal way to dynamically
template a new record based on an existing one, without a too many fussy
illegalities. That could improve readability, particularly in object
initializations. Though clearly it's easier said than done, and I wonder if it
will be worth it once we work all of the kinks out.
Ultimately I do think that...
type R (Ref: access Integer) is record X,Y,Z: Integer; end record;
A: R := (...);
B: R := (A with delta Ref => I'Access);
... is more readable than...
B: R := (Ref => I'Access, X => A.X, Y => A.Y, Z => A.Z);
.. but I suppose your mileage may vary. I'm sure a codebase somewhere will
inevitably end up with something like:
B: R := (A with delta Ref => A.Ref, X => A.X, Y => A.Y);
****************************************************************
From: Tucker Taft
Sent: Monday, April 27, 2020 8:57 PM
Thanks for the support. But unfortunately, or perhaps fortunately, Randy and
Steve together found some "killer" problems in attempting to change any
discriminant in a delta aggregate. One of the problems is that you can write a
delta aggregate for a type that has some ancestors that are private extensions
(and the ultimate ancestor could be a private type). That makes it impossible
to know whether a given discriminant is used to constrain the subtype of some
hidden component (without breaking privacy, that is). I had forgotten that even
an access discriminant can be used to constrain a component of a record-ish
type, meaning that any sort of discriminant can have discriminant-dependent
components. It is very awkward to alter a discriminant that has
discriminant-dependent components, especially if you don't even know how many
there are.
So I have abandoned this effort. Along the way I became more concerned that a
delta aggregate preserves the tag of the "base" expression, even when it is of a
specific type. I believe that is generally not what you want to do, and I know
it introduces significant overhead for all delta aggregates of a tagged type.
Hopefully we can discuss that issue during the ARG meeting, though our agenda is
pretty full!
****************************************************************
From: Richard Wai
Sent: Monday, April 27, 2020 9:03 PM
Indeed I saw that, talk! Unfortunately my email got caught in the spam filter
and was actually from sometime last week!
****************************************************************
From: Tucker Taft
Sent: Friday, April 24, 2020 12:14 PM
Working as a very effective tag team, Randy and Steve convinced me that my hope
of changing an access discriminant, or heaven forbid other sorts of
discriminants, in a delta aggregate, was doomed. The key feature of record
delta aggregates that I had forgotten is that they are permitted on types that
have an ancestor that is a private type or a private extension. The only
requirement is that at least one ancestor is a record or record extension.
Hence, there can be many components that are not visible at the point of the
delta aggregate. This means that those hidden components might use the
discriminant (including an access discriminant) as part of a constraint on the
component's subtype. So even an access discriminant can have
discriminant-dependent components. Groan.
Anyway, so I give up on this idea.
However... along the way I came to fully understand the implications of the
statement that the tag of the delta aggregate (presuming it is tagged) comes
from the tag of the base object, even for an aggregate of a specific type. I
think we should reconsider this, and instead say that the tag comes from the
type of the aggregate, presuming it is of a specific tagged type. This is for
various reasons:
1) Preserving the tag of the base object is significantly more complex, and
means the size (and tag) of the delta aggregate is never known at compile-time.
You always need to do a dispatching call on the 'Size of the base object (after
implicitly converting it to class-wide) to determine the size you need to
allocate. This means that switching a type from untagged to tagged completely
changes the performance impact of using a delta aggregate, when in fact
preserving the tag is rarely of any interest at the place where the delta
aggregate is created.
2) The only other place we preserve a tag is X'Old, which is allowed only in the
assertion expression of a postcondition. We know that efficiency concerns are
generally secondary for postconditions, as they often repeat some of the logic
of the associated subprogram just to be able to describe its effects. In all
other contexts, when you have a "specific-type" view of an object, the tag is
irrelevant. If I write:
Y : T := X;
the tag of X is irrelevant, presuming T is of a specific type. The only time
the tag becomes relevant is if you convert to class-wide in anticipation of
re-dispatching. We are providing a class-wide version of the delta aggregate,
so you could very easily convert X to T'Class if you wanted its tag to be
relevant to the result.
3) Normal use-cases of the delta aggregate, even those involving a base object
of X'Old, are not interested in the tag. E.g.:
procedure P(X : in out T)
with Post => X = (X'Old with A => 3, B => 4);
Furthermore, if you do take a delta aggregate, and convert it to class-wide
after constructing it, you are effectively claiming that no changes were made to
the "extension" of the base object. E.g.
with Post => T'Class(X) = T'Class(T'(X'Old with A => 3, B => 4))
is kind of a weird thing to claim, since if we actually did some re-dispatching
inside P passing along T'Class(X), how do we know what changes were made to the
"extension" of X?
If we really *do* know that nothing was done to the extension of X, it would
seem much more natural to write:
with Post => T'Class(X) = ( T'Class(X'Old) with A => 3, B => 4);
where we have a class-wide type for the delta aggregate itself.
---
So I believe preserving the tag (along with all of the unknown extension parts) of the base object is most likely not what the programmer wants, and also makes it significantly more expensive to use delta aggregates in "everyday" (non-postcondition) code with tagged types than with untagged types.
****************************************************************
From: Randy Brukardt
Sent: Friday, April 24, 2020 2:17 PM
I can go either way on this one. The implemention doesn't change a lot either
way, and the examples I could come up with where it matters mainly seem to
demonstrate why redispatching is a bad idea. :-)
One point thought:
>If we really *do* know that nothing was done to the extension of X, it would
>seem much more natural to write:
>
> with Post => T'Class(X) = ( T'Class(X'Old) with A => 3, B => 4);
>
>where we have a class-wide type for the delta aggregate itself.
It might be "natural", but it's also clearly illegal since the expression of an
extension aggregate cannot be dynamically tagged (one has to know the exact
extensions involved) - see 4.3.1(5/3). I think the type of the aggregate also
has to be specific.
The delta aggregate is at least legal and with understandable semantics, which
seems like an advantage. ;-)
One annoyance here is that the rules would have to be subtly different for
classwide delta aggregates and specific tagged delta aggregates. But that's
already pretty common in Ada (see function results), and in a lot of cases where
there's no specific classwide rule is because classwide isn't allowed at all. So
this isn't much of an issue either way.
****************************************************************
From: Tucker Taft
Sent: Friday, April 24, 2020 2:55 PM
>>If we really *do* know that nothing was done to the extension of X, it would
>>seem much more natural to write:
>>
>> with Post => T'Class(X) = ( T'Class(X'Old) with A => 3, B => 4);
>>
>>where we have a class-wide type for the delta aggregate itself.
It might be "natural", but it's also clearly illegal since the expression of an
>extension aggregate cannot be dynamically tagged (one has to know the exact
>extensions involved) - see 4.3.1(5/3). I think the type of the aggregate also
>has to be specific.
Oops! Forgot the word "delta." I meant for that to be a (class-wide) record
delta aggregate:
with Post => T'Class(X) = ( T'Class(X'Old) with delta A => 3, B => 4);
>The delta aggregate is at least legal and with understandable semantics, which
>seems like an advantage. ;-)
Please read the word "delta" in the above example. I forgot "delta" in my other
examples as well. I have edited history below by inserting it where it was
missing in my e-mail.
>One annoyance here is that the rules would have to be subtly different for
>classwide delta aggregates and specific tagged delta aggregates.
It seems like it only affects the statement about the value of the tag of the
result. It might actually simplify wording elsewhere, though not by much
either.
>But that's already pretty common in Ada (see function results), and in a lot of
> cases where there's no specific classwide rule is because classwide isn't
> allowed at all. So this isn't much of an issue either way.
Agreed.
****************************************************************