carbon-language / carbon-lang

Carbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)
http://docs.carbon-lang.dev/
Other
32.28k stars 1.48k forks source link

Dependent name lookup in base class templates #3196

Open josh11b opened 1 year ago

josh11b commented 1 year ago

Summary of issue:

Consider this code:

// #1
fn F();
class C(template T:! type) {
  extend base: T;
  fn G[self: Self]() {
    // #2
    F();
  }
}

class B {
  // #3
  fn F();
}

var x: C(B) = {};
x.G();

What happens at #2 in x.G()? Does it call #1? #3? is it an error since both names are in scope?

In discussion on 2023-09-05, we decided we were most interested in three options:

Details:

Note that switching T to a checked-generic T:! type means name lookup no longer depends on the instantiation, and so there is a better option for avoiding this problem than C++.

The difference between the first two options is what happens with non-templated base classes:

class D {
  extend base: B;
  fn H[self: Self]() {
    // #4
    F();
  }
}

With C++ rules, #4 would see #1 and #3.

With "no unqualified name lookup through extend", #4 would not consider #3 and instead always resolve to #1. To call #3, you would have to write something like B.F(), Self.F(), or self.F(). We might also consider adding base.F() or even Base.F(). With this rule, unqualified name lookup would only find names directly declared in class scope, and not in any referenced or nested scope.

josh11b commented 1 year ago

C++ Rules

josh11b commented 1 year ago

No unqualified name lookup through extend

josh11b commented 1 year ago

Dependent unqualified name lookup

josh11b commented 1 year ago

Require disambiguation anytime it could look inside a template

josh11b commented 1 year ago

FYI, one use case for base class templates is implementing types that have different APIs for different specializations, such as std::vector<bool>. This might be modeled in Carbon as:

interface VectorSpecialization {
  let BaseType:! type;
  // anything else that might change with specializatoin
}

impl [forall T:! Type] T as VectorSpecialization {
  class BaseType {
    // default API if not specialized
  }
}

impl bool as VectorSpecialization {
  class BaseType {
    // Vector(bool)-specific API;
    fn Flip[addr self: Self*]();
    // ...
  }
}

class Vector(T:! type) {
  extend base: (T as VectorSpecialization).BaseType;
  // ...
}

I agree that in this case we generally aren't going to need to find members of BaseType when doing unqualified lookup in the implementation of Vector(T) methods, and callers of functions like Flip are going to be in a qualified context, which argues against the "Dependent unqualified name lookup" option (and would make the "Require disambiguation anytime it could look inside a template" option more painful for Vector(T)).

The alternative to this specialization approach would require accessing the Flip method through a member of Vector(T), which is a bigger difference from C++.

josh11b commented 1 year ago

@zygoloid @chandlerc I thought of an argument for a particular approach. Right now name lookup with templates follows the information accumulation principle. By this I mean:

Applying this same approach to this problem gives:

So the cases are:

I think that means that in all cases unqualified names are looked up at definition time, and based on the result they get a qualification. I think the remaining possible instantiation/monomorphization errors are all errors that could otherwise occur from template instantiation/monomorphizaton.

Example of the middle case:

interface I {
  // #4
  fn F();
}

class C2(template T:! I) {
  extend base: T;
  fn G[self: Self]() {
    // #5
    // At definition time, `F` resolves to #4 `I.F`
    // based on the `I` bound on `T`.
    F();
  }
}

class B2 {
  // #6
  fn F();
  impl as I {
    // #7
    fn F();
  }
}

// Okay: `B2` implements `I`
var x2: C2(B2) = {};
// Monomorphization error: #5 resolved to `I.F` at
// definition time, which is #7 for `B2`, but #5
// resolves to #6 at instantiation time.
x2.G();

class B3 {
  extend impl as I {
    // #8
    fn F();
  }
}
// Okay: `B3` implements `I`
var x3: C2(B3) = {};
// Okay: #5 resolved to #8 at definition and
// instantiation time.
x3.G();