Associated constants
Table of contents
- Overview
- Declaration checking
- Specifying rewrite constraints
- Definition of associated constant values
- Use of associated constants
Overview
Note: This document only describes non-function associated constants.
An associated constant is declared within an interface scope with the syntax:
[MODIFIERS] let NAME:! TYPE [= INITIALIZER] ;
Associated constants introduce a slot in the witness table for an interface that contains a value of type TYPE.
Associated constants are always generic entities, because they’re always parameterized at least by the Self type of the interface, as well as any other enclosing generic parameters. Note that the interface itself is not parameterized by its Self.
Associated constant entities are held in the associated_constants value store as objects of type AssociatedConstant. Each declaration of an associated constant is modeled by an AssociatedConstantDecl instruction. Each such instruction is then wrapped in an AssociatedEntity instruction which represents the slot within an interface witness where the constant’s value can be found.
Declaration checking
Because associated constants share the syntax of let declarations, a lot of the checking logic is also shared. This logic is in handle_let_and_var.cpp. Associated constant declaration handling proceeds as follows:
let NAME:! TYPE [= INITIALIZER] ; ^StartAssociatedConstantis called at the start of an interface-scopeletdeclaration. This:- Starts a generic declaration region.
- Pushes an instruction block to hold instructions within the declaration of the constant. These form the body of the generic.
let NAME:! TYPE [= INITIALIZER] ; ~~~~^~~~~~~Process the symbolic binding pattern. This is done in handle_binding_pattern.cpp, which detects that we are at interface scope, and creates an
AssociatedConstantDecland correspondingAssociatedConstantentity. This binding is then produced as the instruction associated with the binding pattern.Note: This is somewhat unusual: usually, a pattern instruction would be associated with a pattern parse node.
let NAME:! TYPE ; ^ let NAME:! TYPE = INITIALIZER ; ^When we reach the end of the pattern in an interface-scope
letbinding, either because we reached the=or because we reached the;and there was no initializer,EndAssociatedConstantDeclRegionis called. This:- Ends the generic declaration region.
- Builds an
AssociatedEntityobject, reserving a slot in the interface’s witness table for the constant. - Adds the associated constant to name lookup.
Note: The pattern might not be valid for an associated constant. In this case, we won’t have built an
AssociatedConstantDeclin the previous step. When this happens, we instead just discard the generic declaration region and continue. The invalid pattern will be diagnosed later.let NAME:! TYPE = INITIALIZER ; ^If there is an initializer, we start the generic definition region.
let NAME:! TYPE [= INITIALIZER] ; ^At the end of the declaration,
FinishAssociatedConstantis called to finalize the declaration. This:- Diagnoses if the pattern handling didn’t create an
AssociatedConstantDecl. - Finishes handling the initializer, if it’s present:
- Converts the initializer to the type of the constant.
- Ends the generic definition region.
- Pops the inst block created by
StartAssociatedConstantand attaches it to theAssociatedConstantDecl. - Adds the
AssociatedConstantDeclto the enclosing inst block.
- Diagnoses if the pattern handling didn’t create an
Specifying rewrite constraints
TODO: Fill this out. In particular, note that we do not convert the rewrite to the type of the associated constant as part of forming a where expression if the constant’s type is symbolic, and instead defer that until the facet type is resolved.
Definition of associated constant values
Associated constant values are stored into witness tables as part of impl processing in impl.cpp.
TODO: Fill this out once the new model is implemented.
Use of associated constants
The work to handle uses of associated constants starts in member_access.cpp.
When an AssociatedEntity is the member in a member access, impl lookup is performed to find the corresponding impl witness. The self type in impl lookup depends on how the member name was found.
Simple member access
In LookupMemberNameInScope, if lookup for y in x.y finds an associated constant from interface I, then a witness is determined as follows:
- If the lookup scope is the type
Tofx, then:- If
Tis a non-type facet, the witness for that facet is used. TODO: That facet might not contain a witness forI. In that case we will need to perform impl lookup forT as Iinstead. - Otherwise, impl lookup for
T as Iis performed to find the witness.
- If
- If the lookup scope is
xitself, then:- If
xis a facet type or a namespace, impl lookup is not performed, and the result is simplyy. This happens for cases such asInterface.AssocConst. - Otherwise,
xmust be a type other than a facet type, and impl lookup forx as Iis performed to find the witness.
- If
Compound member access
In PerformCompoundMemberAccess for x.(y), if y is an associated constant then impl lookup is performed for T as I, where T is the type of x and I is the interface in which y is declared to find the witness containing the constant value.
Forming the constant value
Once the witness is determined, AccessMemberOfImplWitness is called to find the value of the associated constant in the witness. In the case where an impl lookup is needed, PerformImplLookup calls AccessMemberOfImplWitness, otherwise it’s called directly.
AccessMemberOfImplWitness uses GetTypeForSpecificAssociatedEntity to form the type of the constant. This substitutes both the generic arguments (if any) for the interface and the Self type into the type of the associated constant. Then, an ImplWitnessAccess instruction is created to extract the relevant slot from the witness. Constant evaluation of this instruction reads the associated constant from the witness table.