!standard 3.9.2(10/2) 18-10-14 AI12-0243-1/02 !standard 3.4(17/2) !standard 3.4(20) !standard 4.1.4(3/2) !standard 4.9.1(2/5) !class Amendment 18-01-11 !status work item 18-05-07 !status Hold by Letter Ballot failed (8-3-0) - 18-05-07 !status work item 18-01-11 !status received 17-10-20 !priority Very_Low !difficulty Medium !subject Subtypes as primitive arguments !summary Add attribute 'Corresponding_Subtype, and allow non-first subtypes as controlling parameters, so long as they have differ by dynamic predicates (not other sorts of constraints) from the first subtype. !problem The use of subtypes with dynamic predicates can be used to factor out common checks from the preconditions of an interface. This reduces code duplication and reduces the chance of forgetting a check. The technique is important enough that an example was provided in the Standard, in the examples for subclause 3.2.4. It shows a hypothetical update to Text_IO using subtypes with dynamic predicates to provide most of the precondition checks needed. However, if the type in question is tagged, Ada does not allow controlling operands to have non-first subtypes. This means that the technique cannot be used on the primitive operations of a tagged type. !proposal (See Wording.) !wording Replace 4.1.4(3/2): Attribute_designator ::= identifier[(static_expression)] | Access | Delta | Digits | Mod with Attribute_designator ::= identifier[(static_expression | subtype_mark)] | Access | Delta | Digits | Mod ---- Add after 3.4(17/2) (that is, just before we start talking about the profile of an inherited subprogram): Given - a subtype D which statically matches the first subtype of a tagged type T; and - a subtype S of a type P which is either the parent type of T [(this case is only possible if T has a parent type)] or a progenitor type of T [(this case is only possible if T has at least one progenitor type)]. the attribute D'Corresponding_Subtype (S) is defined and denotes a subtype of T. This subtype is implicitly declared as subtype is D; if S statically matches the first subtype of P and otherwise as subtype is D with Dynamic_Predicate => P () in S; where the use of in the predicate aspect expression refers to the current instance of the subtype. [In the former case, D'Corresponding_Subtype (S) statically matches D.] In the latter case, predicate checks are enabled for D'Corresponding_Subtype (S) if and only if they are enabled for S. ---- Replace 3.4(20): If the derived type is a record extension, then the corresponding subtype is the first subtype of the derived type. with If the derived type is a record extension or a private extension then the corresponding subtype is D'Corresponding_Subtype(S), where D is the derived type and S is the subtype of the parent or progenitor type. --- Replace the first sentence of 3.9.2(10/2): In the declaration of a dispatching operation of a tagged type, everywhere a subtype of the tagged type appears as a subtype of the profile (see 6.1), it shall statically match the first subtype of the tagged type. with: In the declaration of a dispatching operation of a tagged type, everywhere a subtype of the tagged type appears as a subtype of the profile (see 6.1), it shall be constrained if and only if the first subtype of the tagged type is constrained. [AARM note: in other words, it shall not be subject to any discriminant constraint that the first subtype is not subject to. However, there may be predicates that apply to it and do not apply to the first subtype.] --- Add after 4.9.1(2/5): If subtypes D1 and D2 statically match, subtypes S1 and S2 statically match, and D1'Corresponding_Subtype(S1) is defined (see 3.4), then D1'Corresponding_Subtype(S1) and D2'Corresponding_Subtype(S2) statically match. AARM note: This goes without saying in the case where S1 statically matches the first subtype of its type. In the other case, this rule is equivalent to saying that the predicates for the two Corresponding_Subtype subtypes "come from the same declarations". !example package Pkg1 is type Parent is tagged ... ; function Is_Wonderful (X : Parent) return Boolean; subtype S is Parent with Dynamic_Predicate => Is_Wonderful (S); procedure Op (Y : in out S); -- previously illegal, now legal end Pkg1; with Pkg1; package Pkg2 is type Ext is new Pkg1.Parent with ... ; overriding procedure Op (Y : in out Ext'Corresponding_Subtype (Pkg1.S)); end Pkg2; !wording ** TBD. !discussion Tucker sent the following explanation of the existing restriction: The "technical reasons" relate to the need to perform constraint checks at a call site. This is true for dispatching operations as well -- the checks are performed *before* dispatching to the chosen body. If we start having dispatching operations with non-first-subtypes for parameter subtypes, then you have to be sure that the parameters subtypes in an overriding of a dispatching routine have exactly the same checks required at the point of call. This would imply defining statically matching subtypes of different types, which seemed more of a challenge than we were prepared for when Ada 95 was designed. ------ If we are going to allow type T1 (...) is tagged ... ; subtype S1 is T1 ... ; -- may impose either constraints or predicates procedure Prim (X1 : S1); -- a dispatching operation type T2 (...) is new T1 with ... ; subtype S2 is T2 ... ; -- may impose either constraints or predicates overriding procedure Prim (X2 : S2); then the property we want to ensure (via some appropriate compile-time rules) is that for any value X2 of type T2, (X2 in S2) = (T1 (X2) in S1) An initial approach to this problem was more ambitious; it would have allowed discriminant constraints, as in type T1 (D : Boolean) is tagged ... ; subtype T1_False is T1 (False); procedure Op (T1_False : S1); type T2 is new T1; subtype T2_False is T1 (False); overriding procedure Op (X : T2_False); which seems reasonable enough until we start thinking about more complicated cases like type T1 (D1, D2 : Integer) is tagged null record; subtype S1 is T1 (123, 456); procedure Primitive_Op (X : S1); Type T2 (D3 : Integer) is new T1 (D1 => 123, D2 => D3) with null record; type T3 is new T2 with null record; type T4 (D4 : Integer) is new T3 (D3 => 456); type T5 is new T4 (789); overriding procedure Primitive_Op (X : T5); -- legal? and then we start thinking about private extensions and disabled predicates and it all gets very complicated. So, for now at least, we do not entirely eliminate the rule In the declaration of a dispatching operation of a tagged type, everywhere a subtype of the tagged type appears as a subtype of the profile (see 6.1), it shall statically match the first subtype of the tagged type. Instead, we relax this rule with respect to predicates, but not with respect to discriminant constraints. And instead of looking at two subtypes and trying to decide whether they satisfy the desired (X2 in S2) = (T1 (X2) in S1) property that was mentioned earlier, we define a new Corresponding_Subtype attribute that has the desired property by definition. ---- In some cases, one can use a class-wide type to work-around the restriction. Also, one can write separate preconditions for every primitive subprogram, including a membership in the dynamic predicate(s), rather than using the more convenient parameter subtype(s). !ASIS None needed. !ACATS test An ACATS C-Test is needed to check that the new capabilities are supported. !appendix !topic Subtypes as primitive arguments !reference Ada 2012 RM !from Victor Porton 17-10-21 !keywords subtype subprogram argument primitive subprogram !discussion I propose to allow subtypes (for example subtypes with predicates) as controlling primitive subprogram arguments. A subtype ST of type T for argument A should work as the base type T, but with checking if "T(A) in ST" before doing dispatch (and raising an exception otherwise). This is probably especially useful with predicates. Now as a workaround I replace ST with ST'Class in my code. **************************************************************** From: Tucker Taft Sent: Friday, October 20, 2017 4:28 PM There are some technical reasons why dispatching operations were restricted to having "first" subtypes for their controlling operands. Can you give some concrete examples where using (non-first) subtypes, perhaps with predicates, would be necessary to implement a given abstraction? **************************************************************** From: Randy Brukardt Sent: Friday, October 20, 2017 8:58 PM It might help if you explained the technical reasons, since the reason you would want predicates seems obvious. Consider Text_IO. If File_Type was tagged (which it might have been if defined in Ada 95), then the preferable definition is as described in the RM, 3.2.4(41-51/4): with Ada.IO_Exceptions; package Ada.Text_IO is type File_Type is [tagged] limited private; subtype Open_File_Type is File_Type with Dynamic_Predicate => Is_Open (Open_File_Type), Predicate_Failure => raise Status_Error with "File not open"; subtype Input_File_Type is Open_File_Type with Dynamic_Predicate => Mode (Input_File_Type) = In_File, Predicate_Failure => raise Mode_Error with "Cannot read file: " & Name (Input_File_Type); subtype Output_File_Type is Open_File_Type with Dynamic_Predicate => Mode (Output_File_Type) /= In_File, Predicate_Failure => raise Mode_Error with "Cannot write file: " & Name (Output_File_Type); ... function Mode (File : in Open_File_Type) return File_Mode; function Name (File : in Open_File_Type) return String; function Form (File : in Open_File_Type) return String; ... procedure Get (File : in Input_File_Type; Item : out Character); procedure Put (File : in Output_File_Type; Item : in Character); ... -- Similarly for all of the other input and output subprograms. If tagged is in the declaration of File_Type, this is illegal. One can always use preconditions instead, but that's a lot of extra work and easy to omit in large interfaces. So it would be useful to hear more about the "semantic difficulties" (I remember there were some, but not the details). Especially as you can force constraint checks on derived types, just not on subtypes. (So the implementation work exists, it just isn't useful.) **************************************************************** From: Tucker Taft Sent: Saturday, October 21, 2017 10:00 AM The "technical reasons" relate to the need to perform constraint checks at a call site. This is true for dispatching operations as well -- the checks are performed *before* dispatching to the chosen body. If we start having dispatching operations with non-first-subtypes for parameter subtypes, then you have to be sure that the parameters subtypes in an overriding of a dispatching routine have exactly the same checks required at the point of call. This would imply defining statically matching subtypes of different types, which seemed more of a challenge than we were prepared for when Ada 95 was designed. Now that we have pre/postconditions, etc., we have accepted that wrappers may be necessary in any case. If we are willing to require wrappers due to parameter subtype mismatches, we could probably do that, but it could pose further efficiency challenges for implementations to avoid wrappers when not necessary. **************************************************************** From: Victor Porton Sent: Saturday, October 21, 2017 11:59 AM > Can you give some concrete examples where using (non- > first) subtypes, perhaps with predicates, would be necessary to > implement a given abstraction? https://github.com/vporton/redland-bindings/blob/ada2012/ada/src/rdf-rasqal-query_results.ads Here I define for example: function Get_Binding_Value (Results: Bindings_Query_Results_Type_Without_Finalize'Class; Offset: Natural) return Literal_Type_Without_Finalize; Here Bindings_Query_Results_Type_Without_Finalize is a subtype (with a dynamic predicate) of Query_Results_Type_Without_Finalize. Bindings_* subtype specializes on results which contain so called "bindings". I would like the freedom to write instead: function Get_Binding_Value (Results: Bindings_Query_Results_Type_Without_Finalize; Offset: Natural) return Literal_Type_Without_Finalize; (without 'Class, but with a controlled argument). Honestly speaking, this particular problems seems to be solved by using a class-wide argument instead of a controlling argument. But in other tasks this may probably be not a good solution. **************************************************************** From: Steve Baird Sent: Thursday, June 14, 2018 2:23 PM This AI is about relaxing (in some way) the 3.9.2 rule In the declaration of a dispatching operation of a tagged type, everywhere a subtype of the tagged type appears as a subtype of the profile (see 6.1), it shall statically match the first subtype of the tagged type. We'd like to allow something like the following example (provided by Randy): type File_Type is tagged limited private; subtype Open_File_Type is File_Type with Dynamic_Predicate => Is_Open (Open_File_Type); subtype Writable_File_Type is Open_File_Type with Dynamic_Predicate => Mode (Open_File_Type) = Out_File; procedure Put (File : Writable_File_Type; Item : String); In order to allow this sort of thing, we have to answer (at least) two related questions: 1) What is the corresponding parameter subtype for the corresponding inherited subprogram associated with an extension of this type. 2) What are the "subtype conformance" rules for overriding such an inherited subprogram? More generally, if we are going to allow type T1 (...) is tagged ... ; subtype S1 is T1 ... ; -- may impose either constraints or predicates procedure Prim (X1 : S1); -- a dispatching operation type T2 (...) is new T1 with ... ; subtype S2 is T2 ... ; -- may impose either constraints or predicates overriding procedure Prim (X2 : S2); then the property we want to ensure (via some appropriate compile-time rules) is that for any value X2 of type T2, (X2 in S2) = (T1 (X2) in S1) . Consider the case which is legal today, where S1 statically matches T1 and S2 statically matches T2. The desired property certainly holds in that case because both sides of the equation are always True. An inelegant, but effective, way to do this is to require that S2 must be declared as subtype S2 is T2 with Dynamic_Predicate => (T1 (S2) in S1); Something like this is probably the right approach to take if we just want to minimize complexity (both for the language definition and for compilers and related tools). [Although we'd probably at least want to allow a subtype which statically matches a subtype whose declaration has the prescribed form.] This is a bare-bones solution. However, this doesn't seem very Ada-ish. It would disallow some cases that it would be nice to allow, such as: type T1 (Flag : Boolean) is tagged null record; subtype S1 is T1 (True); procedure Prim (X1 : S1); type T2 is new T1 with null record; subtype S2 is T2 (True); overriding procedure Prim (X2 : S2); We could instead introduce the notion of a "statically matching descendant subtype", defined in such a way that S2 in the above example would be a statically matching descendant subtype of S1. The idea is that if S1 is a subtype of a tagged type T1 and S2 is a subtype of a type T2, then "statically matching descendant subtype" would be defined in such a way that the desired (X2 in S2) = (T1 (X2) in S1) property always holds for any X2 in T2. This could be done, but there are a lot of interactions with discriminants that would need to be ironed out. It would get messy because there are a lot of cases to consider; it is not clear that this is worth the effort. For example, consider type T1 (D1, D2 : Integer) is tagged null record; subtype S1 is T1 (123, 456); procedure Prim (X1 : S1); type Intermediate (D3 : Integer) is new T1 (D1 => D3, D2 => 456); type T2 is new Intermediate with null record; subtype S2 is T2 (D3 => 123); overriding procedure Prim (X2 : S2); -- legal? In some discussions with Randy a while back I started looking at what a definition of "statically matching descendant subtype" might look like. A version which didn't talk about private extensions, formal types, or predicates was already fairly complicated. We also discussed the question of what should be done with something like type T1 (D : Boolean) is tagged null record; subtype S1 is T1 (True); procedure Prim (X : S1); type T2 is new T1 (False); If this example is legal, then what is the subtype of the formal parameter of T2's inherited Prim procedure? It would be nice to avoid all this discriminant-related complexity. This suggests an intermediate approach: relax the current 3.9.2 restriction as it applies to predicates, but not as it applies to discriminants. I believe that this might be the right approach to take, but I'd like to get some feedback from the group before fleshing out the details. **************************************************************** From: Randy Brukardt Sent: Thursday, June 14, 2018 4:56 PM > I believe that this might be the right approach to take, but I'd like > to get some feedback from the group before fleshing out the details. This sounds like a plan to me. One can (almost?) always write a discriminant constraint as a predicate in a pinch. Of course, that suggests that you can't quite eliminate discriminant headaches this way. Although we might simply not care about cases like the ones you gave above: type T1 (D1, D2 : Integer) is tagged null record; subtype S1 is T1 with Dynamic_Predicate => S1.D1 = 123 and S1.D2 = 456; procedure Prim (X1 : S1); type Intermediate (D3 : Integer) is new T1 (D1 => D3, D2 => 456); type T2 is new Intermediate with null record; subtype S2 is T2 with Dynamic_Predicate => S2.D3 = 123; overriding procedure Prim (X2 : S2); -- legal? Probably wouldn't allow this, and it wouldn't be much of a loss. and the biggie: type T1 (D : Boolean) is tagged null record; subtype S1 is T1 with Dynamic_Predicate => T1.D = True; procedure Prim (X : S1); type T2 is new T1 (False); It appears that this latter would work, but the predicate would always be False. Not the most useful thing, but at least no semantic problem. **************************************************************** From: Steve Baird Sent: Friday, October 5, 2018 4:56 PM The attached is a new, less ambitious attempt at this AI. [This is version /02 of this AI - Editor.] Recall that Ada currently requires that if a primitive operation of a tagged type has a parameter or function result of that tagged type, then the parameter/result subtype must match the first subtype of the tagged type. In particular, it cannot be subject to any discriminant constraints or predicates that the first subtype is not subject to. We initially looked at simply eliminating this restriction. The interactions with discriminants turned out to be complicated. So this latest attempt retains the restriction on discriminant constraints but relaxes the restriction as it applies to predicates. It also introduces a new subtype-valued attribute, Corresponding_Subtype. This allows accepting Randy's example (which was discussed earlier) type File_Type is tagged limited private; subtype Open_File_Type is File_Type with Dynamic_Predicate => Is_Open (Open_File_Type); subtype Writable_File_Type is Open_File_Type with Dynamic_Predicate => Mode (Open_File_Type) = Out_File; procedure Put (File : Writable_File_Type; Item : String); where an extension of File_Type which overrides the inherited Put procedure might look like type My_File_Type is new File_Type with ... ; overriding procedure Put (File : My_File_Type'Corresponding_Subtype (Writeable_File_Type); Item : String); Thanks to Randy for some preliminary review, but (as usual) don't blame him if you think the idea stinks or has big holes in it. ****************************************************************