This proposal updates the criteria for when a facet type is considered
"identified." Specifically, it relaxes the requirement for named constraints,
allowing them to be incrementally identified inside the definition, rather than
requiring them to be fully complete, when used through the Self facet. This
change enables impl lookups with Self within a constraint's definition to
correctly resolve witnesses based on prior require impls statements in the
definition.
Under the rules established in Proposal #5168, a facet type is identified only if all its referenced interfaces are declared and all its referenced named constraints are complete.
This definition creates a circularity problem during the definition of a named
constraint. If a require impls statement inside a named constraint definition
relies on an impl lookup with Self, that lookup will fail because the facet
type of Self is not identified before the named constraint is complete. This
prevents require impls statements in a named constraint from depending on
earlier ones.
We propose redefining the identification criteria for facet types by introducing a new partially identified state.
A facet type can be in one of three states: unidentified, partially identified, or identified. A facet type's identifiedness is the minimum of that of its constituents:
The change from previous rules is that a facet type containing a named constraint is now partially identified inside the definition of that named constraint.
As in #5168, an impl declaration and require
statement each requires its constraint to be identified.
We define the rules for facets in impl lookups, which are representable as
<self> as <target facet type> conversions as follows:
In particular, this means that Self as I inside the definition of a named
constraint N may use any require impls statements before that use of Self
in order to provide a witness for I.
To improve diagnostics, we also propose to disallow using a named constraint
inside its own definition, except through the type of Self. Any other use is
diagnosed as an error. This provides a clear error when a named constraint
appears in the constraint of a require statement inside its definition.
SelfThis change allows the compiler to treat a named constraint as partially identified for the purposes of impl lookup while it is still being typechecked.
As the compiler processes a series of require impls statements within a named
constraint, the partially identified facet type of the named constraint, which
can be accessed through Self, is built up incrementally. The partially
identified facet type of Self will contain interfaces provided by
require impls statements written prior to that use of Self. Thus later
require impls statements can use the partially identified facet type of Self
to find witnesses provided by earlier require impls statements during impl
lookup.
The following example demonstrates a scenario that is currently invalid but would be enabled by this proposal:
interface Y {}
interface NeedsY(T:! Y) {}
constraint W {
require impls Y;
// This requires an impl lookup where the query self value is `Self`
// (which is of type `W`) and the query interface is `Y`. The lookup
// requires identifying the facet type of `Self` to find a witness.
// After this proposal, W is partially identified because it has begun being
// defined. The lookup for `Self as Y` can now succeed due to the previous
// `require impls` statement.
require impls NeedsY(Self);
}
In this example, identifying W while it is being defined allows the lookup for
Self as Y in order to form a facet value for NeedsY to succeed because the
compiler knows Self (of type W) implements Y from the previous
require impls statement.
If a facet type for a named constraint was considered identified (not partially identified) inside its definition, the following becomes possible:
constraint W {
require C impls W;
require impls Z;
}
This says that C must implement W, yet W is not fully defined. At that
line W is still empty, so it places no requirements on C. The next line
requires that anything implementing W must implement Z.
To prevent this, the constraint of a require statement must still be
identified, and the facet type of the being-defined named constraint is only
partially-identified.
Note this also disallows the use of W through an alias:
constraint W;
alias X = W;
constraint W {
// Error: X refers to named constraint `W` that is not identified.
require impls X;
}
By requiring the target facet type of an impl lookup to be identified, we disallow an incomplete named constraint from being part of the target of an impl lookup.
The result of an impl lookup stores witnesses for the target facet type. If the target facet type was partially identified, the same facet type may have a different set of interfaces later. A change in the set of interfaces would invalidate the set of stored witnesses.
For example, if this was allowed:
interface Z {}
interface X {}
constraint W;
class C(T:! W) {}
class D {}
interface Y(T:! type) {}
constraint W {
require impls Z;
// Constructs a `D as W` facet value.
require impls Y(C(D));
require impls X;
}
In this example the argument to C will be D as W which will store a witness
for each interface in the identified facet type W. If we allow a partially
identified facet type, then it will store a witness for the interface Z. But
later uses of the facet value in Y(C(D)) would expect witnesses for Z, Y
and X, leading to unsoundness.
This change aligns with Carbon's goal of Code that is easy to read, understand, and write, by allowing expressive generics as they have been designed.
This follows the Information accumulation principle by increasing the information available to the program with each statement.
Initial versions of this proposal did not differentiate between partially identified and identified. This led to unsoundness by allowing conversion to a facet with the incomplete named constraint in the facet's type, as described in Disallowing conversions to incomplete named constraint.
SelfWe considered restricting the use of the partially identified facet type to only
be on the Self facet value. This provided a way to reduce exposure of the
partially identified facet type. But by differentiating the the partially
identified state from identified, we can form the rules around the state of the
facet type instead of the identity of the facet.
We considered allowing conversions from N & J to N & K where N is
partially identified, and J and K are identified.
It seems possible to support this for symbolic facets, by not storing the set of witnesses in the facet value. If the facet type is partially identified, we can defer the collection of witnesses, and just store the facet types that the facet was converted from. In this model, the facet type itself acts as a type of witness that we will will be able to find a witness later once the facet type is identified.
We leave this to a future proposal if and when we find this additional complexity worth adding to the language model.