Update toolchain implementation documentation for associated constants to match the implementation (#7835)

We've made quite a few changes to how associated constants are
implemented since this was written; update the doc to match.

Assisted-by: Claude via Antigravity
This commit is contained in:
Richard Smith
2026-09-28 22:23:59 +00:00
committed by GitHub
parent c1ca91b67a
commit 9d44abd4ee
+83 -77
View File
@@ -28,16 +28,18 @@ _Note:_ This document only describes non-function associated constants.
An associated constant is declared within an interface scope with the syntax:
```carbon
[MODIFIERS] let NAME:! TYPE [= INITIALIZER] ;
[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 constants are declared within the interface-with-self generic, which
is parameterized by the `Self` type of the interface, and is nested within the
interface generic, which is parameterized by any other generic parameters of the
interface. Symbolic instructions within the declaration of an associated
constant, such as those used to compute its type, are part of the
interface-with-self generic.
Associated constant entities are held in the `associated_constants` value store
as objects of type `AssociatedConstant`. Each declaration of an associated
@@ -50,80 +52,76 @@ be found.
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](/toolchain/check/handle_let_and_var.cpp). Associated
constant declaration handling proceeds as follows:
[handle_let_and_var.cpp](/toolchain/check/handle_let_and_var.cpp). The parser
produces distinct parse nodes for a `let` declaration in an interface scope, and
associated constant declaration handling proceeds as follows:
1. ```carbon
let NAME:! TYPE [= INITIALIZER] ;
let NAME: TYPE [= INITIALIZER] ;
^
```
`StartAssociatedConstant` is called at the start of an interface-scope `let`
declaration. This:
The handler for `AssociatedConstantIntroducer` is called at the start of
the 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.
- Pushes an instruction block to hold instructions within the declaration
of the constant.
- Performs the same setup as for other `let` declarations, such as
starting a full pattern and an expression region for the type.
2. ```carbon
let NAME:! TYPE [= INITIALIZER] ;
~~~~^~~~~~~
let NAME: TYPE [= INITIALIZER] ;
~~~~^~~~~~
```
Process the symbolic binding pattern. This is done in
Process the name and type. This is done by the handler for
`AssociatedConstantNameAndType` in
[handle_binding_pattern.cpp](/toolchain/check/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.
which creates an `AssociatedConstantDecl` and corresponding
`AssociatedConstant` entity. This instruction is then produced as the
pattern for the declaration.
3. ```carbon
let NAME:! TYPE ;
^
let NAME:! TYPE = INITIALIZER ;
^
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:
When we reach the end of the pattern, either because we reached the `=` or
because we reached the `;` and there was no initializer, the full pattern
is ended and `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.
- Builds an `AssociatedEntity` object, reserving a slot in the
interface's witness table for the constant.
- Adds the associated constant to name lookup.
4. ```carbon
let NAME:! TYPE = INITIALIZER ;
^
let NAME: TYPE = INITIALIZER ;
^
```
If there is an initializer, we start the generic definition region.
If there is an initializer, the handler for `AssociatedConstantInitializer`
starts processing it, in the same way as for other `let` declarations.
5. ```carbon
let NAME:! TYPE [= INITIALIZER] ;
^
let NAME: TYPE [= INITIALIZER] ;
^
```
At the end of the declaration, `FinishAssociatedConstant` is called to
finalize the declaration. This:
At the end of the declaration, the handler for `AssociatedConstantDecl`
finalizes 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.
- If the pattern is an error, marks the interface-with-self scope as
having an error, and discards the instruction block.
- Otherwise:
- If there is an initializer, converts it to the type of the
constant, and stores the result as the `default_value_id` of the
`AssociatedConstant`.
- Pops the instruction block created in step 1 and attaches it to
the `AssociatedConstantDecl`.
- Adds the `AssociatedConstantDecl` to the enclosing instruction
block.
## Specifying rewrite constraints
@@ -153,35 +151,43 @@ depends on how the member name was found.
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 the type `T` of `x`, then `PerformImplLookup` is
called to perform impl lookup for `T as I`.
- 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.
- If `x` is a namespace or a facet type other than `type`, 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, and `PerformImplLookup` is called to
perform impl lookup for `x as I`.
### 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.
from interface `I`, then `GetAssociatedValueImpl` converts `x` itself to a facet
value of type `I`, and performs impl lookup for `x as I` to find the witness
containing the constant value. The same logic is used by `GetAssociatedValue`,
which finds the value of an associated entity for a given type or facet without
going through member access syntax.
_Note:_ This differs from the handling of associated functions that are
instance methods, for which impl lookup is performed for `T as I`, where `T`
is the type of `x`.
### 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.
Once the witness is determined, a specific for the interface-with-self generic
is formed by `MakeSpecificWithInnerSelf` from the specific for the interface and
the self facet value. For simple member access, this happens in
`PerformImplLookup`, which then calls `AccessMemberOfImplWitness`. For compound
member access, this happens directly in `GetAssociatedValueImpl`.
`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.
`GetTypeForSpecificAssociatedEntity` is then used to form the type of the
constant by substituting the interface-with-self specific 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.
If the access appears within a `where` expression that has a rewrite constraint
for the same associated constant, the `ImplWitnessAccess` is wrapped in an
`ImplWitnessAccessSubstituted` instruction that also records the rewritten
value.