Version 1.1 of ai05s/ai05-0153-1.txt
!standard 3.2.2(7) 09-05-27 AI05-0153-1/01
!class Amendment 09-05-27
!status work item 09-05-27
!status received 09-05-27
!priority Medium
!difficulty Medium
!subject User-defined constraints
!summary
!problem
Ada's constraints are a powerful way to enhance the contract of an
object (including formal parameters). But the constraints that can
be expressed are limited.
For instance, it isn't possible to specify that a record type may
have any of several discriminant values - for discriminants we can
only specify a single value or allow all discriminants.
!proposal
Add user-defined constraints to the language.
!wording
Replace 3.2.2(7) with:
composite_constraint ::=
index_constaint | discriminant_constraint | user_constraint
Add a new clause 3.12:
[Editor's note: This belongs in the non-existent "composite type"
clause. It doesn't make sense to put it into the array or discriminated
type clauses, as it applies to both. I put this last in section 3 to
avoid renumbering.
3.12 User constraints
A user_constraint provides an arbitrary user-created constraint
for a composite or access type.
user_constraint ::= when *Boolean_*expression
Legality Rules
A user_constraint is only allowed in a subtype_indication whose subtype_mark
denotes either an unconstrained array subtype, an unconstrained discriminated
subtype, or an unconstrained access subtype whose designated subtype is
an unconstrained array or discriminated subtype. However, in the case of an
access subtype, a user_constraint is legal only if any dereference of a value
of the access type is known to be constrained (see 3.3). In addition to the
places where Legality Rules normally apply (see 12.3), these rules apply also
in the private part of an instance of a generic unit.
AARM Reason:
We do not allow a user_constraint on a scalar subtype, as scalar subtypes can
be constrained multiple times. That works well for ranges, where it is only
necessary to check the bounds of the most recent constraint (and to check that
those bounds are inside of any existing constraint when the subtype is declared).
But arbitrary expressions (such as every odd value) are not so obvious. We could
have just required compilers to check all of the possible constraints in
succession, but that could add a lot of compiler and runtime overhead. It doesn't
seem useful enough for that.
We also don't allow user_constraints on types that have no constraints at all.
That would essentially allow constraining constrained subtypes, which is
uncomfortable. [Editor's note: See the !discussion below.]
See 3.7 for an explanation of the need for the restriction on access subtypes.
End AARM Reason.
!discussion
This proposal seems similar to type invariants on the surface. However,
these two constructs solve different problems. A type invariant is a
requirement on all values of a type outside of the type's defining
package(s). In particular, it does not vary depending on the view of
an object. A constraint (including user-defined constraints) is a
requirement on a particular view of an object. It can be different
on different views of the same object (as in a formal parameter). Thus
it can be used to specify temporary or transient requirements on an
object.
---
The syntax was chosen to be similar to that proposed for preconditions.
We also considered using "with" instead of "when":
subtype Callable_Symbol is Symbol_Rec with
(Symbol_Rec.D = Func or Symbol_Rec.D = Proc);
or a new keyword "requiring":
subtype Callable_Symbol is Symbol_Rec requiring
(Symbol_Rec.D = Func or Symbol_Rec.D = Proc);
We also considered requiring a user_constraint to be a Boolean function
with single parameter of the appropriate type. This would be easier to
explain the visibility rules for, but again it would be unnecessarily
different than preconditions.
---
We do not allow user_constraints on scalar types, because such constraints
can be combined, and the compiler would have to check all of the constraints
in succession:
subtype Odd is Natural when (Odd mod 2) = 1;
subtype Triple is Odd when (Triple mod 3) = 0;
OK_Obj : Triple := Func_returning_3;
Bad_Obj : Triple := Func_returning_6; --
Both constraint expressions would have to be evaluated in order to make the check
(in general, the compiler cannot tell how the expressions relate).
We also do not allow user_constraints on access types. This seems like a loss,
but it is necessary to keep those constraints from being regularly violated.
We have rules strictly limiting discriminant_constraints on access types, and
those problems occur doubly for user_constraints (which do not necessarily
only have to depend on discriminants; they can also depend on other components).
For instance:
procedure Test1 is
type Rec is record
A : Natural;
end recordl
type Acc_Rec is access Rec;
Obj : aliased Rec := (A => 10);
Ptr : Acc_Rec when ((Acc_Rec.A mod 10) = 0) := Obj'access;
begin
Obj := (A => 5);
if (Ptr.A mod 10 /= 0) then
Put_Line("What kind of constraint is this??");
end if;
end Test1;
--!corrigendum H.4(19)
!ACATS test
Add ACATS B and C tests for this feature.
!appendix
This AI was created after discussion at the Tallahassee ARG meeting; it
is partially based on an old idea found in AC-0157.
From the Tallahassee minutes about type Invariants:
Randy asks whether the user-defined constraint idea is worth looking at
(either as an alternative or replacement). After discussion, we decide
that it seems to solve a somewhat different problem - it allows adding
contracts to particular parameters, objects, etc. User-defined constraints
would be a way to deal with non-contiguous sets of discriminants, one-sided
array constraints, and so on. There is sufficient interest to have that
written up (it previously was discussed on Ada-Comment and filed as AC-0157).
It's not very necessary on scalar types, so if the rules get too messy for
them, don't allow them. (Randy notes when writing up these minutes that that
would probably be a generic contract problem.) Steve notes that it would
need a bounded error if the expression does not return the same value
when called with the same value (we would want to be able to eliminate
duplicate checks) -- the bounds are that the check is either made or not.
****************************************************************
From: Randy Brukardt
Date: Wednesday, May 20, 2009 5:28 PM
At the last meeting, I was directed to write a proposal about user-defined
constraints. I was told to try to restrict them only to composite types,
because the "satisfability" rules would be hard to make work for scalar types,
while they are pretty much right for composite types already.
To do that brings up a generic contract model issue. We would need to use
an assume-the-worst rule for generic bodies. Essentially, any subtype that
is of a generic formal type (or a type derived from such a type) could not
have a user-defined constraint (since we don't allow reconstraining or use
on types that might not be composite). It would be possible to move those
constraints to the private part, however.
Is the latter workaround enough? It seems like it would be to me (it is the
same workaround we suggest for 'Access, for instance), but I wanted to get
other people's opinions on that before I spend a lot of effort writing up
the proposal. (I was going to ask the accessibility subcommittee in one of
our periodic calls, but we never got to it.)
****************************************************************
From: Randy Brukardt
Date: Wednesday, May 27, 2009 11:59 PM
Here is a more fundamental question about user-defined constraints: Do they act
like real constraints (that is, they apply to the view during the entire time of
its existence) or do they only apply at the point that the language defines
subtype conversions?
If they act like real constraints, then they can only depend on bounds and
discriminants; that seems very limiting. (Only bounds and discriminants have the
necessary rules to prevent changes between checks.)
If they are just checks applied at particular points, then we have anomalies
where "constrained" values does not satisfy the constraint. Moreover, whether or
not Constraint_Error is raised can depend on the parameter passing mode:
type Rec is record
A : Natural;
end record;
subtype Decimal_Rec is Rec when Rec.A mod 10 = 0;
Obj : Decimal_Rec := (A => 10);
procedure Do_It (P : in out Decimal_Rec) is
begin
P.A := 5;
end Do_It;
Do_It (Obj); -- (1)
The call at (1) will raise Constraint_Error if Obj is passed by copy: the copy
back will fail the subtype conversion back to the original object. But if Obj is
passed by reference, the view conversion will succeed and there will not be any
check after the call.
I was considering only allowing these constraints on private types, but that is
uncomfortable for two reasons (1) you lose capability when you see the full type
[note however that this is the same rule implied for type invariants: they can
only be given for a private type]; (2) it still allows the body (anywhere in the
scope of the full declaration, in fact) to break the constraint.
package Pack is
type Priv is private;
function Is_Decimal (P : Priv) return Boolean;
function Decimal (N : Natural) return Priv;
procedure Half (P : in out Priv);
private
type Priv is record
A : Natural;
end record;
end Pack;
package body Pack is
function Is_Decimal (P : Priv) return Boolean is
begin
return P.A mod 10 = 0;
end Is_Decimal;
function Decimal (N : Natural) return Priv is
begin
return (A => N*10);
end Decimal;
procedure Half (P : in out Priv) is
begin
P.A := P.A / 2;
end Half;
end Pack;
with Pack;
procedure Test2 is
subtype Decimal_Priv is Pack.Priv when Pack.Is_Decimal (Decimal_Priv);
Obj : Decimal_Priv := Pack.Decimal(1);
begin
Pack.Half (Obj); -- (2)
end Test2;
(2) is very much like (1); the user constraint is only checked on return if Priv
is passed by copy. So Obj most likely does not meet its constraint after the
call to Pack.Half.
We could probably fix this particular problem with some additional checking rule
on "in out" and "out" parameters, but I worry that this is just the camel's nose
-- it might turn into a mess of creeping additional run-time checks (especially
once Steve starting thinking about it).
I originally ran across this issue thinking about the limitations of access
types constrained by user-defined constraints. But eventually I realized that
the issue was pretty general -- it's not just a problem with designated objects
of access types.
So what do you all think? Should I write this up:
(1) Allowing these to apply only to bounds and discriminants? (The issues with
those are well-understood, of course.)
(2) Allowing these to apply only to private types, with suitable additional
checks defined on the return from the "defining subsystem" (the package
containing the private type and its subsystem). The constraints would not
meaningfully apply within the defining subsystem (it would be easy to change
an object after the check).
(3) Combining (1) and (2).
(4) Neither (1) nor (2). (a) I've got a better idea; (b) don't bother.
****************************************************************
Questions? Ask the ACAA Technical Agent