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

need name shadowing rules for Self #3714

Open
zygoloid opened this issue Feb 22, 2024 · 11 comments
Open

need name shadowing rules for Self #3714

zygoloid opened this issue Feb 22, 2024 · 11 comments
Labels
leads question A question for the leads team

Comments

@zygoloid
Copy link
Contributor

Given

class A {
  impl i32 as AddWith(Self) {
    // ... Self ...
  }
}

... does Self inside the impl refer to A, to i32, or is it invalid to use it due to ambiguity? Same question applies for Self on the right-hand side of the as. Explorer treats Self here as being A -- an impl only introduces a Self if there's not already a Self in scope.

The same question arises for nested classes:

class A {
  class B {
    // ... Self ...
  }
}

I think I would expect that Self is B within class B, rather than naming A or being ambiguous. Especially given that a lookup for Self within the body of B should presumably behave the same way if B is defined out of line:

class A {
  class B;
}
class A.B {
  // ... Self ...
}

... and it would be very surprising if Self here didn't work.

Presumably interfaces can be nested within classes, where the same question would arise:

class A {
  interface B {
    // ... Self ...
  }
}

Is Self now the type implementing the interface, or is it A? I think in this case it must be the type implementing the interface, because there would otherwise be no way to name that type.

Some options to consider:

  1. A class, interface, or impl declaration always introduces Self, as the class type or implementing type. Self refers to the innermost such declaration.
  2. A class or interface always introduces Self. An impl only introduces Self if there's no Self in scope.
  3. An impl doesn't introduce Self ever.
@josh11b
Copy link
Contributor

josh11b commented Feb 22, 2024

Another option is that an impl declaration doesn't introduce Self, but Self is introduced in the body of an impl definition.

I am not excited about option 3, since I think Self will shorten long type names in many circumstances, and no Self would make it hard to copy from the interface definition into the impl body.

@zygoloid
Copy link
Contributor Author

We have some examples in explorer's prelude that would be broken by Self coming into scope at the { rather than the as, for example:

impl String as EqWith(Self) {

As the type here gets more complicated, we might feel more pressure to make something like this work. @chandlerc observed that we use .Self for similar things in other contexts, so we could introduce the type on the left of the as as .Self on the right-hand side:

impl String as EqWith(.Self) { ... }

// Note that this already works -- `.Self` is in scope after `where`.
impl String as AddWith(String) where .Result = .Self { ... }

// And this already works.
let T:! EqWith(.Self) = String;

It might be a bit strange that we can use .Self between as and {, but Self within { ... }, but I think the general rules here are:

  • You can use .Self in contexts that expect a constraint, and it refers to the constrained type.
  • You can use Self within the braces of a declarative block to refer to the type whose API is described by that block.

One other thing we might want to consider: should (String as EqWith(.Self)).Op be valid? Currently, .Self is not in scope on the right-hand side of an as operator in an expression, but it would be useful in a case like this for it to be valid, and that would be consistent with the treatment of impl ... as suggested above.

@chandlerc
Copy link
Contributor

FWIW, I like this last idea (including saying (String as EqWith(.Self)).Op is a thing).

As for where all this would apply -- whenever we are constraining a type? That seems to cover the :! bindings, where clauses, impl T as, and the as operator here?

The only part I struggle with is the spelling itself of .Self. But while I struggle with it in some ways, in most ways it is better than the alternatives I can come up with. And if we ever want to change the spelling, that seems better and easily done orthogonally at some point so hopefully there's no need to gate on it.

zygoloid added a commit to zygoloid/carbon-lang that referenced this issue Mar 27, 2024
Per carbon-language#3714, some of the details here are not yet settled. In particular,
we might want `Self` to come into scope at the start of the definition,
not at the `as` keyword. However, this change allows us to accept the
uncontroversial examples.
github-merge-queue bot pushed a commit that referenced this issue Mar 27, 2024
Per #3714, some of the details here are not yet settled. In particular,
we might want `Self` to come into scope at the start of the definition,
not at the `as` keyword. However, this change allows us to accept the
uncontroversial examples.
@justzh

This comment was marked as spam.

@chandlerc
Copy link
Contributor

Should this be a question for leads @zygoloid ? And do we have an answer?

Do we need a proposal?

@zygoloid zygoloid added the leads question A question for the leads team label Apr 8, 2024
@zygoloid
Copy link
Contributor Author

zygoloid commented Apr 8, 2024

Yes, I think we have enough analysis here that we can reasonably make this a leads question and answer it. I think the resolution we've converged on is:

  • Self comes into scope at the { of an interface, impl, class, or mixin definition, and shadows any outer Self. (We could either say that Self is a member name, or that it's added to the scope directly, depending on whether we want T.Self to work as a way to name T.)
  • .Self comes into scope at an as (in an impl or in an as expression), :!, or where, and names "the thing on the left" -- in impl T as X, T as X, and T:! X, .Self in X refers to T, and in A where B, the type T:! A constrained by the where clause is .Self in the constraints in B.

Previously we've said that if there's more than one .Self in scope, they must all agree, in some loose sense (they can have different facet types so long as they have the same value). I'm not sure whether we still want that, or how we'd implement it if so -- maybe a shadowing rule for .Self would be more consistent.

@zygoloid
Copy link
Contributor Author

zygoloid commented Apr 8, 2024

I think we need a proposal to explore the details, but we can probably answer the broad questions as above.

@chandlerc
Copy link
Contributor

Previously we've said that if there's more than one .Self in scope, they must all agree, in some loose sense (they can have different facet types so long as they have the same value). I'm not sure whether we still want that, or how we'd implement it if so -- maybe a shadowing rule for .Self would be more consistent.

This is probably one of the details to work out in a proposal, but freely shadowing seems both logical and reasonable, but potentially confusing. Specifically, I'm imagining a case like: A where .T = Foo(.Self) and .U = Bar(.T as Baz(.Self)) -- we need a pretty simple rule for whether the second .Self refers to A.T or A and to try to avoid confusion because of the nearness of the two usages if they differ.

@josh11b
Copy link
Contributor

josh11b commented Apr 17, 2024

Previously we've said that if there's more than one .Self in scope, they must all agree, in some loose sense (they can have different facet types so long as they have the same value). I'm not sure whether we still want that, or how we'd implement it if so -- maybe a shadowing rule for .Self would be more consistent.

This is probably one of the details to work out in a proposal, but freely shadowing seems both logical and reasonable, but potentially confusing. Specifically, I'm imagining a case like: A where .T = Foo(.Self) and .U = Bar(.T as Baz(.Self)) -- we need a pretty simple rule for whether the second .Self refers to A.T or A and to try to avoid confusion because of the nearness of the two usages if they differ.

Would it be reasonable to say that .Self must "approximately agree" within an expression, but would otherwise shadow .Self from an enclosing construct?

@chandlerc
Copy link
Contributor

Previously we've said that if there's more than one .Self in scope, they must all agree, in some loose sense (they can have different facet types so long as they have the same value). I'm not sure whether we still want that, or how we'd implement it if so -- maybe a shadowing rule for .Self would be more consistent.

This is probably one of the details to work out in a proposal, but freely shadowing seems both logical and reasonable, but potentially confusing. Specifically, I'm imagining a case like: A where .T = Foo(.Self) and .U = Bar(.T as Baz(.Self)) -- we need a pretty simple rule for whether the second .Self refers to A.T or A and to try to avoid confusion because of the nearness of the two usages if they differ.

Would it be reasonable to say that .Self must "approximately agree" within an expression, but would otherwise shadow .Self from an enclosing construct?

Where do we get two .Selfs that might need to shadow but don't come from within an expression? I had thought my example was all a single type expression, and even a single where clause "expression" such as it is?

Oh, are you specifically suggesting that within a particular branch of a where clause the .Selfs agree? That would suggest the first .Self in my example refers te A and the second to .T?

@josh11b
Copy link
Contributor

josh11b commented May 17, 2024

I was just mistaken.

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