Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do we match forward declarations with their definitions? #1132

Closed
josh11b opened this issue Mar 15, 2022 · 4 comments
Closed

How do we match forward declarations with their definitions? #1132

josh11b opened this issue Mar 15, 2022 · 4 comments
Labels
leads question A question for the leads team

Comments

@josh11b
Copy link
Contributor

josh11b commented Mar 15, 2022

With the decision on #472 , Carbon has separate forward declarations from definitions and we need some way to match them up. This comes up for functions, classes, interfaces, named constraints, interface implementations, and probably others.

There are a three main components to this question:

  1. What parts of a declaration are used to connect a forward declaration to its definition?
  2. What constitutes a match for any given part?
  3. Once we have identified that a declaration should be associated with a particular definition, what additional checking must be done to make sure they are consistent?
@josh11b
Copy link
Contributor Author

josh11b commented Mar 15, 2022

In open discussion on 2022-03-14 we came to some initial conclusions:

What parts of a declaration are used to connect a forward declaration to its definition?

The principle we wanted to follow for this is to make it as minimal as we can. This is intended to give the compiler the best change at producing good diagnostics. For most cases, this means we match based on only the name. The main exception is for constructs that don't have a name, like interface implementations.

What constitutes a match for any given part?

There is a spectrum here, from textual matching to semantic matching. Textual matching is very simple, both to understand and to implement in the compiler, but has two downsides:

  • It can be incorrect if the declaration and definition are in different scopes and so the same word would mean different things in the two different contexts.
  • It can lack expressivity in cases where there are two different textual representations that are semantically equivalent.

These can be addressed by semantic matching, but measuring complete semantic equivalence is prohibitive from a compiler implementation perspective and obscures which declarations would match to human readers. Instead we decided on something in between:

  • Names would be resolved (to correctly reflect what scope the name is in)
  • Aliases would be followed (to allow refactoring to happen piecemeal)
  • Parentheses would be canonicalized (since things with different parenthesis but parsed the same should be considered equivalent)

Otherwise Carbon would not attempt to determine any other semantic equivalence.

Once we have identified that a declaration should be associated with a particular definition, what additional checking must be done to make sure they are consistent?

For functions we decided we wanted parameter names to match if they were specified, but they could be omitted. We wanted this consistency based on:

  • the confusion that we've experienced when they don't match (common C++ lint tools ask to make them match)
  • wanting reflection to return a single parameter name for a parameter
  • wanting the parameter names to be consistent with the single docstring we expect to associate with a function

It is still an open question how omitted parameter names would be written (using _: or just dropping them entirely as in C++). Provisionally we will use _:.

In general we wanted to start with a restrictive approach and see how it went. For this reason, we don't think deduced parameters should be allowed to be reordered.

We were undecided about what to do about where clauses in interface implementations. Require them to always be stated? Allow them to be omitted but require them to match if present? Provisionally, I'm going to treat them like parameters: allow them to be omitted using where _, and require them to match if specified.

@chandlerc
Copy link
Contributor

I think we have consensus around the solution @josh11b wrote up. This doesn't include a decision around particular syntax choices (although I'm personally happy w/ what's here at least to start), but includes the rest of this:

  • Names only, whenever named.
  • Resolving names, following aliases, and canonicalizing parentheses
  • Same names when introducing names or ignored.

Re-open if more detailed things come up that we need to handle.

@zygoloid
Copy link
Contributor

What agreement rule do we want for the case where the forward declaration and definition are in different scopes -- specifically, when checking whether a declaration of a member function in a class (or a defaulted function in an interface or a member of an impl) agrees with a definition written outside that class? For example:

class A {
  fn F();
}

let T:! type = A;
// OK?
fn T.F() {}

class B(T:! type) {
  fn F();
}
constraint Type {}

// OK?
fn B(T:! Type).F() {}

I suggest we require the part before the . to agree with the class declaration, in the same way that we require the part after the . to agree with the function declaration, so both of the above definitions are rejected -- and in general, the first part of fn <tokens that identify a class>.F() is allowed if and only if class <tokens that identify a class>; would be allowed. That seems to be in line with what's proposed here, but @josh11b and I weren't certain the prior decision covered this.

@chandlerc
Copy link
Contributor

I agree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team
Projects
None yet
Development

No branches or pull requests

4 participants