The name of an impl in class scope

Pull request

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:

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.