The name of an impl
in class
scope
Table of contents
Abstract
class C {
impl as I;
}
is redeclared
impl C.(as I)
for purposes of match_first
/impl_priority
blocks and definitions.
Problem
An impl
declaration can be declared in class
scope:
class C {
alias T = bool;
impl as As(T);
}
or in file scope:
class C {
alias T = bool;
}
alias T = i32;
impl C as As(T);
These impl
declarations need to be named so they may be redeclared in a definition or match_first
/impl_priority
block. Under the current rules introduced in proposal #1084 and modified in #3763, an impl
redeclaration must match syntactically, and that only works if the redeclaration enters the same scope as the original declaration.
This problem is demonstrated in the example above. We need some indication whether to lookup T
in the file or the class scope, otherwise these both would be redeclared as impl C as As(T)
.
class C(T:! type) {
class E {}
impl E as I(C(T), E);
}
// No ability to do syntactic match
// C(T) does not match C(T:! type)
// C(T).E does not match E
impl forall [T:! type] C(T).E as I(C(T), C(T).E);
Background
The need for forward declarations of entities comes from the information accumulation principle.
Leads issue #1132 defined the initial rules for matching forward declarations to their definitions. Those rules were partially incorporated into the design by proposal #1084, “Generics details 9: forward declarations”.
A replacement approach was discussed on 2024-03-11 and mentioned in proposal #3763. This is the approach of syntactic matching and re-entering the same scope. The syntax adopted by this proposal was first suggested in those.
Proposal #3762: Merging forward declarations and proposal #3980: Singular extern
declarations refined the rules for forward declarations, including the rules for extern
declarations.
Leads issue #5251: impl
declarations in a generic class context is (pending resolution) saying that an impl
declaration in class scope must use Self
in a deducible position.
Proposal
An impl
declaration is associated with the scope it is first declared in, and can only be redeclared in that scope, matching all other declarations. Consider how a function is redeclared outside the scope of a class in which it was originally defined:
class Z(T:! type) {
// Forward declaration of a function.
fn F();
}
// Definition of the function that was forward declared.
fn Z(T:! type).F() { ... }
To redeclare an impl
after the end of the scope it was declared in, that scope may be re-entered as part of the impl
redeclaration, in the same way, except with parentheses around the name of the impl
, as in:
class X {
// Forward declaration that `X impls Y`:
impl as Y;
}
// Definition of the `impl` that `X impls Y`
// that was forward declared in `X`:
impl X.(as Y) { ... }
More generally, in a class
scope
class __X__ {
impl __Y__;
}
is redeclared impl __X__.(__Y__)
outside of that class
scope. Here __X__
is whatever sequence of tokens appears in that position in the class
declaration, and __Y__
is the sequence of tokens in the impl
declaration. These declarations are matched syntactically, and anything in __Y__
is interpreted as if it appeared in the scope of __X__
like it was first declared.
Details
Here are some examples of this in practice:
class F {}
class A {
impl as As(i32);
impl Self as As(bool);
impl A as As(f64);
impl F as As(A);
class G {}
impl G as As(A);
}
impl A.(as As(i32)) { ... }
impl A.(Self as As(bool)) { ... }
impl A.(A as As(f64)) { ... }
impl A.(F as As(A)) { ... }
impl A.(G as As(A)) { ... }
Parameterized classes:
class B(T:! type) {
impl B(i32) as AddWith(A(T));
}
impl B(T:! type).(B(i32) as AddWith(A(T))) { ... }
Parameterized impl:
class C {
impl forall [T:! type] as I(T);
}
impl C.(forall [T:! type] as I(T));
Putting the forall
inside the parens both simplifies the syntactic match and means that any mentions of names in that clause are in scope. For example:
class D(T:! type) {
class E {}
impl forall [U:! J(E)] as I(U);
}
impl D(T:! type).(forall [U:! J(E)] as I(U));
Notice how the E
in the constraint on U
is found in the D(T:! type)
scope.
Nested classes:
class C1 {
class C2 {
class C3 {
impl as I;
class C4 {}
}
}
}
// Defining impl that was forward declared within the `C3` definition:
impl C1.C2.C3.(as I) { ... }
// Defining a new impl:
impl C1.C2.C3.C4 as I { ... }
Notice that we don’t know which form will be used until we see:
- the open paren (
(
) after a.
, - a parameter pattern,
- a non-parameter argument, or
- the
as
.
Optional Self
before as
The normalization to add Self
before as
when that type is omitted before performing syntactic match is preserved from proposals #1084 and #3763. Note that the Self
is inserted in the parentheses when the as
appears there. So:
class A {
// First impl declaration is equivalent
// to `impl Self as As(i32);`
impl as As(i32);
// Second impl declaration.
impl Self as As(bool);
}
// Redeclaration of the first impl declaration.
impl A.(Self as As(i32)) { ... }
// Since this is equivalent to
// `impl A.(Self as As(bool)) { ... }`, is a
// valid redeclaration of the second impl
// declaration.
impl A.(as As(bool)) { ... }
Rationale
The need for this proposal comes from supporting forward declarations for the information accumulation principle. The specifics of this proposal were chosen comply with these Carbon goals:
- Unambiguous rules that are simple to implement benefit language tools and ecosystem.
- Code that is easy to read, understand, and write benefits by having rules that are simple to state and validate. The syntactic match rule means redeclarations are close to a copy of the original declaration, which makes authoring straightforward and automatable.
Alternatives considered
Use semantic match for the scope
Leads issue #5367: impl
in class
redeclaration syntax with parameterization considered an alternative where
// Alternative
impl forall [T:! type] B(T).(as I) { ... }
instead of:
// This proposal
impl B(T:! type).(as I) { ... }
It avoided putting a forall
clause inside the parentheses, but that both greatly limited the syntactic matching that we could do, and meant examples like:
// This proposal
impl D(T:! type).(forall [U:! J(E)] as I(U));
had to instead be written with more qualfiers:
// Alternative
impl forall [T:! type, U:! J(D(T).E)] D(T).(as I(U));
It is not just helpful for the compiler: being able to make fewer and more mechanical changes after copy-pasting the impl
declaration to make the redeclaration makes authoring the code easier, and simplifies tooling to automate the process.