Ada-Rapporteur-Group / User-Community-Input

Ada User Community Input Working Group - Github Mirror Prototype
27 stars 1 forks source link

Iterator over vectors should preserve unconstrained state of elements of discriminated type #12

Open sttaft opened 2 years ago

sttaft commented 2 years ago

If a vector is instantiated with an element type that is an unconstrained-with-defaults discriminated type, the normal way of iterating over such a vector (for E of Vec loop ...) produces elements which are constrained by their current value, unless the type happens to have a partial view. This is a side-effect of the rule that makes dereferences constrained (unless a partial view exists -- see Ada RM 4.1(9/3)), and the use of dereference as part of the Variable_Indexing aspect (Ada RM 4.1.6(3/5)). On the other hand, Replace_Element and Update_Element can change the discriminant.

If vectors are supposed to be as much as possible like arrays, then we should investigate creating an alternative mechanism that preserves the unconstrained-with-defaults property when using user-defined (variable) indexing.

sttaft commented 2 years ago

Here is a mail chain started by Steve Baird about this issue. -Tuck

Stephen Baird Wed, Jul 6, 2:40 PM (3 days ago)

to Randy, me

Someone at AdaCore complained that looping over the elements of an array does not behave the same as looping over the elements of a container with respect to constrainedness of the elements in the case where the element subtype is discriminated, unconstrained, and definite.

 type T (D : Boolean := False) is null record; -- no partial view

 type T1 is array (Character) of T;
 X : T1;

begin for E of X loop E := (D => True); -- discriminant-changing assignment succeeds end loop; end;

If you instantiate Ada.Containers.Vectors instead of declaring an array type, then the analogous loop fails a discriminant check. IIUC, this is because a dereference of an access type is involved and so 4.1(9) applies.

Is this correct? If so, then is it a problem that warrants some action?

Thanks, -- Steve

Tucker Taft taft@adacore.com Wed, Jul 6, 6:17 PM (3 days ago)

to Stephen, Randy

The obvious user workaround is to wrap the discriminated record in a non-discriminated record. As far as the language definition, the description of Vector indicates it is supposed to operate like an array (see A.18.2(2/2)), so I believe GNAT is in the wrong here, and should wrap the element type itself to avoid this problem. We could add an AARM Implementation Note to emphasize this. I don't see a need to add any more normative words.

-Tuck

Stephen Baird Wed, Jul 6, 8:34 PM (3 days ago)

to me, Randy

A.18.2(2) is not normative - it is general introductory text. Citing that does not answer the question.

To repeat the question: Given an instance of Ada.Containers.Vectors where the actual parameter for Element_Type is discriminated, unconstrained, definite, and has no partial view, and given a loop statement of the form for Elem of My_Vector loop Elem := My_Element_Type'(...); end loop; the name of the loop parameter (Elem, in this case) denotes a view of some element of the container. Is that view a constrained view or an unconstrained view?

[In the analogous case for looping over the elements of an array, the answer is "unconstrained".]

ARM 5.5.2(13): For a container element iterator, the loop parameter for each iteration instead denotes, an indexing (see 4.1.6) into the iterable container object for the loop, with the only parameter to the indexing being the current value of the loop cursor for the given iteration;

So in this case, the loop parameter denotes the result of a call to Reference. This then gets interpreted as an implicit dereference via 4.1.5(6): A generalized_reference denotes a view equivalent to that of a dereference of the reference discriminant of the reference object.

Which means that 4.1(9) applies: If the designated subtype has unconstrained discriminants, the (actual) subtype of the view is constrained by the values of the discriminants of the designated object, except when there is a partial view of the type of the designated subtype that does not have discriminants, in which case the dereference is not constrained by its discriminant values.

So the answer to the original question is "constrained".

So does this warrant any action, or do we just say "Yes, that's how it works - arrays and containers are not quite equivalent in this case; if that's really what you want, then declare a one-field wrapper type and use that for your container instances." ?

I could imagine adding some new Boolean aspect for a one-field undiscriminated record type and then saying that if the (sub)type of a generalized_reference would be one of these types, then instead it is that of the one component. Then Ada.Containers.Vectors (along with the other definite-element container specs) could declare a one-field wrapper type and use that as the designated type for the discriminant of type Reference_Type. I'm not claiming that this is elegant, and there would probably be compatibility issues.

-- Steve

Tucker Taft taft@adacore.com Wed, Jul 6, 9:18 PM (3 days ago)

to Stephen, Randy

On Wed, Jul 6, 2022 at 8:34 PM Stephen Baird [baird@adacore.com](mailto:baird@adacore.com) wrote: A.18.2(2) is not normative - it is general introductory text. Citing that does not answer the question.

Well, one could argue it establishes intent. But I agree, we don't always consider introductory text as normative. But usually, we will bracket it if it is redundant with some normative wording.

To repeat the question: Given an instance of Ada.Containers.Vectors where the actual parameter for Element_Type is discriminated, unconstrained, definite, and has no partial view, and given a loop statement of the form for Elem of My_Vector loop Elem := My_Element_Type'(...); end loop; the name of the loop parameter (Elem, in this case) denotes a view of some element of the container. Is that view a constrained view or an unconstrained view?

[In the analogous case for looping over the elements of an array, the answer is "unconstrained".]

ARM 5.5.2(13): For a container element iterator, the loop parameter for each iteration instead denotes, an indexing (see 4.1.6) into the iterable container object for the loop, with the only parameter to the indexing being the current value of the loop cursor for the given iteration;

So in this case, the loop parameter denotes the result of a call to Reference. This then gets interpreted as an implicit dereference via 4.1.5(6): A generalized_reference denotes a view equivalent to that of a dereference of the reference discriminant of the reference object.

Good point! So at least when using the Variable_Indexing aspect are forced into the semantics associated with dereferencing an access type, which implies the view is constrained by its current value.

Which means that 4.1(9) applies: If the designated subtype has unconstrained discriminants, the (actual) subtype of the view is constrained by the values of the discriminants of the designated object, except when there is a partial view of the type of the designated subtype that does not have discriminants, in which case the dereference is not constrained by its discriminant values.

So the answer to the original question is "constrained".

So does this warrant any action, or do we just say "Yes, that's how it works - arrays and containers are not quite equivalent in this case; if that's really what you want, then declare a one-field wrapper type and use that for your container instances." ?

I don't see how to change this without a new approach to Variable_Indexing, which doesn't use an access-type-dereference model. We have on and off talked about supporting the notion of an "aliased return" object, and in that case you could imagine that whether or not it is constrained could be determined by the 'Constrained attribute of the returned object.

Interestingly, I would presume that Update_Element and Replace_Element could be used to change the discriminant of an element, which makes it somewhat more like an array. But going through the Variable_Indexing aspect as currently defined does impose a constrained view due to the access-type-dereference model.

I suppose this is another argument for considering the "aliased return" approach to user-defined (variable) indexing.

I could imagine adding some new Boolean aspect for a one-field undiscriminated record type and then saying that if the (sub)type of a generalized_reference would be one of these types, then instead it is that of the one component. Then Ada.Containers.Vectors (along with the other definite-element container specs) could declare a one-field wrapper type and use that as the designated type for the discriminant of type Reference_Type. I'm not claiming that this is elegant, and there would probably be compatibility issues.

I would rather "fix" Variable_Indexing. This would also help to remove at least one additional case of dynamic accessibility checks, I believe.

-- Steve

Take care,

Randy Brukardt Wed, Jul 6, 9:24 PM (3 days ago)

to Stephen, me

Steve writes:

A.18.2(2) is not normative - it is general introductory text. Citing that does not answer the question.

It's definitely normative text, defining the meaning of "length" and "capacity". But I don't see how that has anything to do with the answer:

To repeat the question: Given an instance of Ada.Containers.Vectors where the actual parameter for Element_Type is discriminated, unconstrained, definite, and has no partial view, and given a loop statement of the form for Elem of My_Vector loop Elem := My_Element_Type'(...); end loop; the name of the loop parameter (Elem, in this case) denotes a view of some element of the container. Is that view a constrained view or an unconstrained view?

The loop parameter part here is somewhat a red herring. The question you want to ask is whether the (dereferenced) result of Reference, and equivalently, the parameter to Update_Element, is a constrained view. And this applies to any generalized indexing, so the loop is just a very small part of the issue.

Similarly, in the array case, it applies to any indexing of the array.

We've always allowed containers to have constrained elements for unconstrained types, but that only applies to Update_Element of Indefinite containers. That seems like a bug, in that a similar restriction should be allowed for the dereferenced result of Reference.

(Walk through the rules to prove the above skipped...)

Which means that 4.1(9) applies: If the designated subtype has unconstrained discriminants, the (actual) subtype of the view is constrained by the values of the discriminants of the designated object, except when there is a partial view of the type of the designated subtype that does not have discriminants, in which case the dereference is not constrained by its discriminant values.

  So the answer to the original question is "constrained".

Interesting. This significantly damages the model of Reference and by correllary generalized indexing in the case of mutable types. The idea was one could assign into such indexes in the saem way as an array. Given the way most compilers implement such types, that's not particularly important (a max size implementation is generally useless since it implies artifical bounds on sizes which rarely work). But it does emasculate the implementation in Janus/Ada (which is correct, IMHO) in the case of containers.

So does this warrant any action,

Yes. The entire intent was that assigning into an indexing of a vector object would work like assigning into an array. If that's not the case, then we have a significant problem. This makes vectors useless with mutable components - a problem we thought we fixed.

(We should research whether this problem was noted when Reference was created and if so, why we let it go. But I'm not going to do that now.)

or do we just say "Yes, that's how it works - arrays and containers are not quite equivalent in this case; if that's really what you want, then declare a one-field wrapper type and use that for your container instances." ?

No, please don't. I want to ultimately get rid of arrays altogether (obviously in a follow-on language), and having containers significantly harder to use is not going to work.

I could imagine adding some new Boolean aspect for a one-field undiscriminated record type and then saying that if the (sub)type of a generalized_reference would be one of these types, then instead it is that of the one component. Then Ada.Containers.Vectors (along with the other definite-element container specs) could declare a one-field wrapper type and use that as the designated type for the discriminant of type Reference_Type. I'm not claiming that this is elegant, and there would probably be compatibility issues.

The above sounds like a hack. We should be able to do better.

My view is that the formal type is essentially a partial view for the type, and it doesn't have any discriminants. We could simply modify 4.1(9) to say that, but I worry that might be too general (we are mostly trying to fix containers here). 4.1(9) really exists to preserve some Ada 83 invariants about allocated objects, but we don't need it outside of that. Still, unintended consequences suggests not going all the way and repealing it altogether (or just in the case of generics).

So I think it would make more sense to define an aspect of an access discriminant that says that it has the properties of a designated type with a partial view regardless of the actual designated type. That shouldn't cause any implementation issues since the discriminant is pointing at an object that already has those properties. (If necessary, the aspect could also be applied to the object/component similar to the way aliased works, but I don't think that is needed.) The actual object would have to be an aliased object (it could not be allocated -- so a wrapper might be needed in the implementation).

The "wrapper" implementation shows that compilers have to somehow be able to implement this, and I don't see much reason that whatever that method is would be an issue so long as the compiler knows it is necessary. We got rid of the rules requiring aliased objects to be constrained long ago, and allocated objects are like that mainly for historical reasons.

This will take some research to figure out if there are any problems, but the less of a hack it is the better. 4.1(9) is a hack, we've already weakened it in some cases, we can weaken it some more in a controlled fashion.

Ultimately, I don't want to make users pay for this glitch, if it costs some work for container implementers, I'll live.

                             Randy.

Randy Brukardt Wed, Jul 6, 9:52 PM (3 days ago)

to me

Tucker writes:

I don't see how to change this without a new approach to Variable_Indexing, which doesn't use an access-type-dereference model.

I don't see why; 4.1(9) already does not apply if the dereferenced type has certain properties. All aliased objects effectively have those same properties, so as long as it is required that the access discriminant is set from an aliased object (possible with an aspect), we don't need 4.1(9) to apply. No need to change Variable_Indexing.

We have on and off talked about supporting the notion of an "aliased return" object, ...

I'm not sure who has been talking about that, but my Ada successor language has "variable returning functions", so I've thought about this quite a bit. (The language has keywords "static", "constant", and "variable", with "constant" being the default, and it makes sense to allow those on function results as well as in object declarations. It also allows overloading on "constant" and "variable", with the rules of generalized indexing to chose between them. I also get rid of a lot of redundant stuff like objects and arrays and generalized references and anonymous access types. Co-derivation takes care of the need for dispatching on references. But I digress.)

Semantically, a variable returning function has essentially the same rules as an anonymous access result, without the need to write 'Access and .all. That especially includes accessibility (unfortunately) - I simplified accessibility by getting rid of types nested in subprograms but it cannot be completely eliminated.

Anyway, my point is that semantically there is almost no difference between an access discriminant and an access result and a variable result. So I don't see much value to greatly changing these things for Ada (for a new language based on Ada, definitely).

...

I would rather "fix" Variable_Indexing. This would also help to remove at least one additional case of dynamic accessibility checks, I believe.

I don't see any compatible way to use some new kind of return type in Variable_Indexing. And you will not succeed at eliminating accessibility checks - at least without abandoning safety. You claimed that you had done that for anonymous access returns, and it turned out that they were way worse than anyone expected. I expect you are making the exact same mistakes. Fool me once, shame on you; fool me twice, shame on me. :-)

Besides, a correct implementation of Reference should not have any dynamic accessibility checks unless the context demands those (should be extremely rare). One does a static accessibility check on the aliased parameter of Reference, and no check at all should be done inside of Reference (since the accessibility of the aliased parameter is always assumed to be returnable -- since the caller checked that before the call).

So I suspect that this would end up being a mainly sideways change: a lot of work with no actual improvement when done. Let's not go there.

                   Randy.

Tucker Taft taft@adacore.com Wed, Jul 6, 10:46 PM (3 days ago)

to Randy

The problem is that an access value could point to a constrained or unconstrained object, and at the moment there is no way to know.

The idea behind "aliased return" was to base them on aliased formals, saying that an aliased return object has the same accessibility as the aliased formal(s), and so the formals are required to be sufficiently long-lived so the use of the return object is safe.

-Tuck

Randy Brukardt Wed, Jul 6, 11:02 PM (3 days ago)

to Stephen, me

[Left Steve out of my earlier reply, sorry about that - RLB]

Tucker writes:

The problem is that an access value could point to a constrained or unconstrained object, and at the moment there is no way to know.

Sure, but I suggested adding an aspect allowing/requiring the object to be unconstrained (that's what we want in cases like this, I think). Such an aspect would turn off 4.1(9) for such access types. The limitation would be that the access would have to be to an aliased object (only allocated objects are constrained by initial values these days). We could even go so far as to allow named types to use that aspect and such types would allocate without being constrained by initial value.

We already "turn off" 4.1(9) if some type happens to have a partial view, so there is no likely implementation problem with doing so.

The idea behind "aliased return" was to base them on aliased formals, saying that an aliased return object has the same accessibility as the aliased formal(s), and so the formals are required to be sufficiently long-lived so the use of the return object is safe.

Sure, but you still need accessibility checks. You certainly can't return a local object that way (and that's banned by an accessibility check), but you could return a global object that way, which would be allowed by an accessibility check (and that would seem useful for dealing with global data structures). And this is exactly how access returns (and access discriminants of newly created immediately returned objects) work today. There is no check for a part of an aliased parameter.

So there would be no difference (other than removing 'Access and .all from the creation and use) -- and not having 'Access and .all as points to make checks could actually make the implementation and definition harder.

This just sounds like a lot of work that ultimately would signify nothing. We should have done it that way in the first place, but it's now rather late in the game for that.

And we're not going to fix the associated overloading and definitional issues, since those would be incompatible on the margins (at a minimum, it would be very hard to prove that they were fixed). Nor operator visibility, unsafe defaults, or lots of other minor but annoying things. So this is better in a new language that is just like Ada but simpler and more general. (I realize that will be a tough sell, but I don't have to sell it to do it. :-)

                  Randy.

Tucker Taft taft@adacore.com Thu, Jul 7, 12:38 PM (2 days ago)

to Randy

On Wed, Jul 6, 2022 at 9:52 PM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote: Tucker writes:

I don't see how to change this without a new approach to Variable_Indexing, which doesn't use an access-type-dereference model.

I don't see why; 4.1(9) already does not apply if the dereferenced type has certain properties. All aliased objects effectively have those same properties, so as long as it is required that the access discriminant is set from an aliased object (possible with an aspect), we don't need 4.1(9) to apply. No need to change Variable_Indexing.

I agree we could invent a new aspect that applies to access types that says they can only point at unconstrained objects. But it feels like adding a bit of a kludge on a kludge.

We have on and off talked about supporting the notion of an "aliased return" object, ...

I'm not sure who has been talking about that, but my Ada successor language has "variable returning functions", so I've thought about this quite a bit.

That is very much the idea of "aliased return." It is an attempt to generalize from the notion of aliased formal parameters, and allow things like user-defined indexing to be defined without ever using access types. The general goal is to eliminate all use of anonymous access types, in favor of aliased formals and aliased return objects.

...

Semantically, a variable returning function has essentially the same rules as an anonymous access result, without the need to write 'Access and .all.

That is an important simplification, from a programmer point of view, and is also a feature of the "aliased return" notion. ... Anyway, my point is that semantically there is almost no difference between an access discriminant and an access result and a variable result. So I don't see much value to greatly changing these things for Ada (for a new language based on Ada, definitely).

I think we could incrementally simplify Ada by shifting to "aliased return" instead of the rather kludgy Implicit_Dereference types with access discriminants et al. I agree there isn't much difference semantically, but it is lower cognitive overhead for the programmer, I would claim.

                   Randy.

Randy Brukardt Thu, Jul 7, 10:26 PM (2 days ago)

to me

Tucker Taft writes:

On Wed, Jul 6, 2022 at 9:52 PM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote:

Tucker writes:

I don't see how to change this without a new approach to Variable_Indexing, which doesn't use an access-type-dereference model.

I don't see why; 4.1(9) already does not apply if the dereferenced type has certain properties. All aliased objects effectively have those same properties, so as long as it is required that the access discriminant is set from an aliased object (possible with an aspect), we don't need 4.1(9) to apply. No need to change Variable_Indexing.

I agree we could invent a new aspect that applies to access types that says they can only point at unconstrained objects. But it feels like adding a bit of a kludge on a kludge.

I understand that, but I think you would need something similar if you wanted "aliased return" to be able to return unconstrained objects. You would have to be able to disallow returning an allocated object, and that would require some special rules that you wouldn't want to apply all of the time. Much more on that below.

We have on and off talked about supporting the notion of an "aliased return" object, ...

I'm not sure who has been talking about that, but my Ada successor language has "variable returning functions", so I've thought about this quite a bit.

That is very much the idea of "aliased return." It is an attempt to generalize from the notion of aliased formal parameters, and allow things like user-defined indexing to be defined without ever using access types. The general goal is to eliminate all use of anonymous access types, in favor of aliased formals and aliased return objects.

That's a fine goal, but it seems too late for Ada. It doesn't make a ton of sense to define a whole new parallel set of capabilities that differ only a tiny amount. (To completely remove anonymous access types, you'd have to come up with procedure types as well, or at least a "nestable" aspect to allow the special semantics of parameters of anonymous access-to-subprogram types. I'd tried to work out rules for the latter, but never could get the accessibility rules quite right.)

In a successor where compatibility is removed from concern, then it should be a mandate. :-)

I came to this area by figuring out how to extend overloading to objects. It would be obvious to treat a declaration like:

 X : constant Natural := Foo;

as equivalent to a function (just as is done for enumeration literals). That is, function X return Natural is (Foo);

However, that doesn't work for variables without some extension. Since I was already proposing a keyword "variable" (so that the default for objects is the safer alternative of "constant"), it would make sense to allow those keywords in function declarations as well:

 X : variable Natural := <>;

as equivalent to:

 function X return variable Natural is ...;

You could, of course, put "constant" (or "static" for that matter) into a function return as well, with the obvious meaning.

Of course, that requires figuring out what a variable-returning function does, and it seems that the semantics of anonymous access returns (minus the access part) would be the right starting point.

...

Semantically, a variable returning function has essentially the same rules as an anonymous access result, without the need to write 'Access and .all.

That is an important simplification, from a programmer point of view, and is also a feature of the "aliased return" notion.

I'm less sure, but clearly the rules would have to be worked out. Is there a Language Study Group in our future?? :-)

            ...

Anyway, my point is that semantically there is almost no difference between an access discriminant and an access result and a variable result. So I don't see much value to greatly changing these things for Ada (for a new language based on Ada, definitely).

I think we could incrementally simplify Ada by shifting to "aliased return" instead of the rather kludgy Implicit_Dereference types with access discriminants et al. I agree there isn't much difference semantically, but it is lower cognitive overhead for the programmer, I would claim.

Definitely sounds like a language study group.

To expand on my point prior. For an anonynous access return, the requirement on the prefix of 'Access is that the object is aliased (with allocated objects being aliased), and that the nominal designated subtypes statically match (or some weaker rules for unconstrained discriminanted objects). That allows an access-to-unconstrained to designate both objects that are truly unconstrained, and objects that are constrained by their initial value. 4.1(9) then essentially assumes-the-worst about the result (assuming it is constrained by its initial value).

An aliased return type would naturally have to work the same way, as it would be bizarre to not allow returning P.all for an aliased T if P designated T. Such a restriction would be an nasty wart on Ada (especially if we still allowed returning P.all.C, which we would need to implement some of the container types). Ergo, I think would have to allow the programmer to request the wart (in exchange for getting an unconstrained object). Alternatively, we could leave the wart and add an aspect to access types to allow them to make allocated objects unconstrained. But there seems to be a need for a kludgy aspect in Ada. (Of course, the real kludge is the "constrained by initial value" nonsense, but that we're stuck with in Ada - a successor would not have to be so constrained - pun intended. :-)

                                   Randy.

Tucker Taft taft@adacore.com Thu, Jul 7, 11:12 PM (2 days ago)

to Randy

With aliased return, my expectation is that the 'Constrained attribute would be defined just as it is for an in-out (aliased) formal, so you could carry along the constrained-ness of the returned object at run-time.

-Tuck

On Thu, Jul 7, 2022 at 10:26 PM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote: Tucker Taft writes:

On Wed, Jul 6, 2022 at 9:52 PM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote:

...

I understand that, but I think you would need something similar if you wanted "aliased return" to be able to return unconstrained objects....

Randy Brukardt Thu, Jul 7, 11:34 PM (2 days ago)

to me

Ugh. That's pretty evil, since making that attribute work is a mass of hacks in Janus/Ada. There is an implicit subtype Param_Uncon that is supposed to be defined with every unconstrained type (and often isn't where it's expected to be); and that subtype is associated with a descriptor block which is passed instead of the object. Certainly a mess that I'd want to avoid with a function return, where it would be even more problematical (since the temporary for the description would have to be heap-allocated and garbage-collected). No idea how that would interact with build-in-place. Additionally, propagating the value of that flag into every assignment thunk is another problem (it gets lost easily).

I'd rather expect it to be a bunch of hacks in any compiler, since it is so far outside of anything expected - you don't want to have to carry a runtime determination of whether an object can be modified - there's no sane place to put it and still generate the necessary composable code. I suppose you could actually have a component with the value in the record, but that would prevent some realistic representations for records (since you'd need a place for that component in all records of the type, some types have no bits to steal).

I'd rather get rid of it altogether, since it is very rarely encountered (even in the ACATS tests it only occurs a couple of times, I remember having a hard time finding a test that used it in order to check it's implementation -- I think I ended up spending several days writing one ultimately). But of course that would require some other solution to this problem.

As I said, sounds like a Language Study Group.

          Randy.

From: Tucker Taft [mailto:taft@adacore.com] Sent: Thursday, July 07, 2022 10:12 PM To: Randy Brukardt Subject: Re: container constrainedness consternation

Tucker Taft taft@adacore.com Fri, Jul 8, 4:50 PM (23 hours ago)

to Stephen, Randy

I agree that the 'Constrained attribute is not used explicitly very often, but it is implicitly tested on any assignment to an in-out formal parameter of a discriminant-with-defaults type. An aliased return is irrelevant to a "build in place" because build in place implies a new object is being created, so the referenced component of the aliased formal would necessarily be copied.

More generally, an "aliased return" is necessarily returning by reference, so it seems fairly straightforward to augment the returned reference with a boolean flag representing 'Constrained, when appropriate. When appropriate, a postcondition could promise "not Func'Result'Constrained".

-Tuck

Stephen Baird Fri, Jul 8, 5:28 PM (23 hours ago)

to Randy, me

So we all agree that there is a problem and that some action is warranted. As Randy said: "sounds like a Language Study Group".


You are both right, introductory text is normative. But in this case I think we all now agree that A.18.2(2) is too general to answer the question at hand, although it does suggest intent. [If it did somehow answer the question, then the same reasoning (whatever that reasoning might be) would presumably also imply allowing slicing of vectors in the case of a single scalar index. But I suppose a lot of other conclusions also follow from "if False".]


I wrote: "I could imagine adding some new Boolean aspect for a one-field undiscriminated record type ..."

I agree that this is ugly and undesirable. If we can come up with a better solution, perhaps based on the "aliased return" idea and changes to the Variable_Indexing attribute, that would be great. That approach sounds promising to me. We can keep my hack in mind as a backup option in case the more elegant approach runs into problems.

  -- Steve

Stephen Baird Fri, Jul 8, 5:30 PM (23 hours ago)

to Randy, me

minor correction: "single scalar index" => "single discrete index" -- Steve

Stephen Baird Fri, Jul 8, 7:35 PM (20 hours ago)

to me, Randy

Tuck wrote:

Interestingly, I would presume that Update_Element and Replace_Element could be used to change the discriminant of an element, which makes it somewhat more like an array.

FWIW, it appears that GNAT implements this correctly (based on one very simple test). -- Steve

Randy Brukardt Fri, Jul 8, 11:20 PM (17 hours ago)

to me, Stephen

Tucker Taft writes:

I agree that the 'Constrained attribute is not used explicitly very often, but it is implicitly tested on any assignment to an in-out formal parameter of a discriminant-with-defaults type.

Right, but that was my point: such an assignment is very rare in real code. (I was not only talking about the attribute, but any use of that attribute's value, implicit or explicit.) I once searched through all of the compilable Ada code I could find (all of RR's code, ASE, and some other sources) for examples of such assignments, and I didn't find anything other than the ACATS test for that case. (Which is an Ada 83 test and has no obvious basis in reality.)

It makes some sense when you think about it: There are a lot of record types in Ada programs, but mutable record types are rarer (a lot of records used don't have discriminants at all). A lot of the parameter passing associated with mutable records tends to be access types designating such types (that's certainly the case in the Janus/Ada compiler, which extensively uses mutable records, but primarily treats the access types as the primary entity). Even so, there are a lot of record types passed as parameters in Ada programs, but the majority of parameters (of any type) are "in" parameters. Even when you have an "in out" or "out" parameter of a mutable record type (or any record type for that matter), one often assigns individual components rather than the full type.

This is a feature that exists and is tested in the ACATS, but it never comes up in practice. The quality of the implementation (or even if there is such an implementation) doesn't really matter.

You are proposing to take a feature that barely ever happens and move it to the center of everything (indexing for containers). Moreover, the feature is a hazard - an exception gets raised only in obscure cases which depend on the actual parameter of a call (very much like the dynamic accessibility checks on anonymous access parameters). It's a bad idea in the first place; especially given how rarely the situation even comes up. I would expect the same here, as we're talking about a failure that would only occur for containers whose elements were a specific kinds of record type, and only if certain other conditions exist. It would be a hard to find bug even in language-defined containers (that at least could be mitigated with an ACATS test), and there's little chance that it would be found in user-built uses.

I would be way happier if we had rules to make these checks at compile-time (probably with an aspect) -- that also is true for parameter passing (although there it clearly would be an option). For most things, the author of the subprogram knows whether or not they expect to modify the discriminant(s) of the parameter (or return); why in the heck should the compiler be bending over backwards (with a noticeable runtime cost) for something that the programmer may not even need??

An aliased return is irrelevant to a "build in place" because build in place implies a new object is being created, so the referenced component of the aliased formal would necessarily be copied.

More generally, an "aliased return" is necessarily returning by reference, so it seems fairly straightforward to augment the returned reference with a boolean flag representing 'Constrained, when appropriate. When appropriate, a postcondition could promise "not Func'Result'Constrained".

Said Boolean flag does not fit in the return register (which is fully taken up by a pointer - at least in typical Windows and Linux calling conventions), so you are forcing a level of indirection where one is not needed. And use of some return memory scheme, whatever is used by the compiler. I suppose one could stick it into the low bit if objects are always aligned, but then you have to mask every use. Yuck.

I'm against extending a bad idea ('Constrained) any further, and certainly not for something that will be commonly used.

We probably should talk about this in some public place, so we keep the discussion around.

              Randy.

Tucker Taft taft@adacore.com 9:07 AM (7 hours ago)

to Randy, Stephen

On Fri, Jul 8, 2022 at 11:20 PM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote: ...

We probably should talk about this in some public place, so we keep the discussion around.

Agreed. We should copy this exchange into a GitHub issue. I'll try to do that at some point.

              Randy.

Take care, -Tuck ...

swbaird commented 2 years ago

Another inelegant but relatively simple approach would be to define a new aspect that can be specified (only) for a reference type whose designated type is (or could me, in the case of some generic formals) a definite discriminated subtype . This would indicate the policy for determining the Constrained attribute of the designated object (if any); choices might include Constrained, Unconstrained, and Dynamic; implementing the Dynamic case would presumably involve an implicit Boolean component; the Unconstrained case would require checks (perhaps compile-time checks only?) when constructing a value of the reference type that the designated object is unconstrained. No such checks would be needed in the unconstrained case. If the designated subtype turns out to be something other than a definite discriminated subtype of a type with no partial view, then the new aspect has impact on program behavior. Interactions with coextensions are left as an exercise for the interested reader. For the reference types defined in the "definite" versions of the predefined Container generics, a policy of Unconstrained could be specified. This is another fallback plan if the "aliased return" idea doesn't work out for some reason. -- Steve

swbaird commented 2 years ago

correction: "... then the new aspect has [no] impact on program behavior".

ARG-Editor commented 2 years ago

The new aspect approach is what I suggested at the start of this discussion. I doubt that it needs to be this complex, though, since the vast majority of the time you know whether you need the item to be unconstrained (so you can change the discriminants), or if you don't care (which is the current situation). So I had just suggested an aspect that you could apply to an access discriminant or possibly an access type to force the objects to be unconstrained (with appropriate compile-time checks).

It would make sense to do this generally, since no one really wants this check done at runtime in the first place. The same logic as above applies to in out parameters of a mutuable type, to access returns (and any replacement feature for them). Some uses will require an unconstrained object, and passing something that isn't unconstrained is just going to be a latent bug (a tripping hazard, as Bob calls it) - one that might fail right away or in some unusual condition months in the future. And other uses have no need to change the discriminant and might as well treat the object as "constrained by the initial value" when it is used, even if it is not. (You do get a runtime failure if you do try to change the discriminant anyway, but that clearly is a bug in that case. Either way, there is much less runtime overhead We're stuck with the runtime overhead case, but it is worthwhile approximately never (much like dynamic elaboration checking - a fully static version is good enough, but Ada still encurrs overhead -- of course, GNAT has a switch to get rid of that).

Not sure how far to go, but this seems more promising to me than "aliased returns" per-se, as this would actually eliminate runtime overhead.

                    Randy.