Associated constants

Table of contents

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:

  1. let NAME:! TYPE [= INITIALIZER] ;
    ^
    

    StartAssociatedConstant is called at the start of an interface-scope let declaration. 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.
  2. 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 AssociatedConstantDecl and corresponding AssociatedConstant entity. 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.

  3. let NAME:! TYPE ;
                    ^
    let NAME:! TYPE = INITIALIZER ;
                    ^
    

    When we reach the end of the pattern in an interface-scope let binding, either because we reached the = or because we reached the ; and there was no initializer, EndAssociatedConstantDeclRegion is called. This:

    • Ends the generic declaration region.
    • Builds an AssociatedEntity object, 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 AssociatedConstantDecl in the previous step. When this happens, we instead just discard the generic declaration region and continue. The invalid pattern will be diagnosed later.

  4. let NAME:! TYPE = INITIALIZER ;
                    ^
    

    If there is an initializer, we start the generic definition region.

  5. let NAME:! TYPE [= INITIALIZER] ;
                                    ^
    

    At the end of the declaration, FinishAssociatedConstant is 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 StartAssociatedConstant and attaches it to the AssociatedConstantDecl.
    • Adds the AssociatedConstantDecl to the enclosing inst block.

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 T of x, then:
    • If T is a non-type facet, the witness for that facet is used. TODO: That facet might not contain a witness for I. In that case we will need to perform impl lookup for T as I instead.
    • Otherwise, impl lookup for T as I is performed to find the witness.
  • If the lookup scope is x itself, then:
    • If x is a facet type or a namespace, impl lookup is not performed, and the result is simply y. This happens for cases such as Interface.AssocConst.
    • Otherwise, x must be a type other than a facet type, and impl lookup for x as I is performed to find the witness.

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.