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

Allow unqualified name lookup for class members #2287

Merged
merged 14 commits into from
Nov 9, 2022
154 changes: 111 additions & 43 deletions docs/design/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Member functions](#member-functions)
- [Class functions](#class-functions)
- [Methods](#methods)
- [Name lookup in member function definitions](#name-lookup-in-member-function-definitions)
- [Deferred member function definitions](#deferred-member-function-definitions)
- [Name lookup in classes](#name-lookup-in-classes)
- [Nominal data classes](#nominal-data-classes)
- [Member type](#member-type)
- [Let](#let)
Expand Down Expand Up @@ -914,71 +915,122 @@ the `me` parameter must be in the same list in square brackets `[`...`]`. The
`me` parameter may appear in any position in that list, as long as it appears
after any names needed to describe its type.

#### Name lookup in member function definitions
#### Deferred member function definitions

When defining a member function lexically inline, we delay type checking of the
function body until the definition of the current type is complete. This means
that name lookup _for members of objects_ is also delayed. That means that you
can reference `me.F()` in a lexically inline method definition even before the
declaration of `F` in that class definition. However, other names still need to
be declared before they are used. This includes unqualified names, names within
namespaces, and names _for members of types_.
When defining a member function lexically inline, the body is deferred and
processed as if it appeared immediately after the end of the outermost enclosing
class, like in C++.

```
For example, given a class with inline function definitions:

```carbon
class Point {
fn Distance[me: Self]() -> f32 {
// ✅ Allowed: `x` and `y` are names for members of an object,
// and so lookup is delayed until `type_of(me) == Self` is complete.
return Math.Sqrt(me.x * me.x + me.y * me.y);
geoffromer marked this conversation as resolved.
Show resolved Hide resolved
}

fn CreatePolarInvalid(r: f32, theta: f32) -> Point {
// ❌ Forbidden: unqualified name used before declaration.
return Create(r * Math.Cos(theta), r * Math.Sin(theta));
}
fn CreatePolarValid1(r: f32, theta: f32) -> Point {
// ❌ Forbidden: `Create` is not yet declared.
return Point.Create(r * Math.Cos(theta), r * Math.Sin(theta));
}
fn CreatePolarValid2(r: f32, theta: f32) -> Point {
// ❌ Forbidden: `Create` is not yet declared.
return Self.Create(r * Math.Cos(theta), r * Math.Sin(theta));
}

fn Create(x: f32, y: f32) -> Point {
// ✅ Allowed: checking that conversion of `{.x: f32, .y: f32}`
// to `Point` is delayed until `Point` is complete.
return {.x = x, .y = y};
}

fn CreateXEqualsY(xy: f32) -> Point {
// ✅ Allowed: `Create` is declared earlier.
return Create(xy, xy);
}
var x: f32;
var y: f32;
}
```

fn CreateXAxis(x: f32) -> Point;
These are all parsed as if they were defined outside the class scope:

fn Angle[me: Self]() -> f32;
```carbon
class Point {
fn Distance[me: Self]() -> f32;
fn Create(x: f32, y: f32) -> Point;

var x: f32;
var y: f32;
}

fn Point.CreateXAxis(x: f32) -> Point {
// ✅ Allowed: `Point` type is complete.
// Members of `Point` like `Create` are in scope.
return Create(x, 0);
fn Point.Distance[me: Self]() -> f32 {
return Math.Sqrt(me.x * me.x + me.y * me.y);
}

fn Point.Create(x: f32, y: f32) -> Point {
return {.x = x, .y = y};
}
```

#### Name lookup in classes

[Member access](expressions/member_access.md) is an expression; details are
covered there. Because function definitions are
[deferred](#deferred-member-function-definitions), name lookup in classes works
the same regardless of whether a function is inline. The class body forms a
scope for name lookup, and function definitions can perform unqualified name
lookup within that scope.

For example:

```carbon
class Square {
fn GetArea[me: Self]() -> f32 {
// ✅ OK: performs name lookup on `me`.
return me.size * me.size;
// ❌ Error: finds `Square.size`, but an instance is required.
return size * size;
// ❌ Error: an instance is required.
return Square.size * Square.size;
// ✅ OK: performs instance binding with `me`.
return me.(Square.size) * me.(Square.size);
// ✅ OK: uses unqualified name lookup to find `Square.size`, then performs
// instance binding with `me`.
return me.(size) * me.(size);
}

fn GetDoubled[me: Self]() -> Square {
// ✅ OK: performs name lookup on `Square` for `Create`.
return Square.Create(me.size);
// ✅ OK: performs unqualified name lookup within class scope for `Create`.
return Create(me.size);
// ✅ OK: performs name lookup on `me` for `Create`.
return me.Create(me.size);
}

fn Create(size: f32) -> Square;

var size: f32;
}
```

The example's name lookups refer to `Create` and `size` which are defined after
the example member access; this is valid because of
[deferred member function definitions](#deferred-member-function-definitions).

However, function signatures must still complete lookup without deferring. For
example:

```carbon
class List {
// ❌ Error: `Iterator` has not yet been defined.
fn Iterate() -> Iterator;

fn Point.Angle[me: Self]() -> f32 {
// ✅ Allowed: `Point` type is complete.
// Function is checked immediately.
return Math.ATan2(me.y, me.x);
class Iterator {
...
}

// ✅ OK: The definition of Iterator is now available.
fn Iterate() -> Iterator;
}
```

**Note:** The details of name lookup are still being decided in issue
[#472: Open question: Calling functions defined later in the same file](https://github.com/carbon-language/carbon-lang/issues/472).
An out-of-line function definition's parameters, return type, and body are
evaluated as if in-scope. For example:

```carbon
// ✅ OK: The return type performs unqualified name lookup into `List` for
// `Iterator`.
fn List.Iterate() -> Iterator {
...
}
```

### Nominal data classes

Expand Down Expand Up @@ -2144,6 +2196,16 @@ the type of `U.x`."
- [Separate "exact" and "or derived" variations on types](/proposals/p0777.md#separate-exact-and-or-derived-variations-on-types)
- [Separate "exact" and "or derived" variations on pointers](/proposals/p0777.md#separate-exact-and-or-derived-variations-on-pointers)

- [#875: Principle: Information accumulation](https://github.com/carbon-language/carbon-lang/pull/875)

- Allow information to be used before it is provided
[globally](/proposals/p0875.md#strict-global-consistency),
[within a file](/proposals/p0875.md#context-sensitive-local-consistency),
or
[within a top-level declaration](/proposals/p0875.md#top-down-with-minimally-deferred-type-checking).
- [Do not allow inline method bodies to use members before they are declared](/proposals/p0875.md#strict-top-down)
- [Do not allow separate declaration and definition](/proposals/p0875.md#disallow-separate-declaration-and-definition)

- [#981: Implicit conversions for aggregates](https://github.com/carbon-language/carbon-lang/pull/981)

- [Field order is not significant](/proposals/p0981.md#field-order-is-not-significant)
Expand All @@ -2166,12 +2228,18 @@ the type of `U.x`."
- [Make `Self` a member of all types](/proposals/p2107.md#make-self-a-member-of-all-types)
- [`where` operator could be associative](/proposals/p2107.md#where-operator-could-be-associative)

- [#2287: Allow unqualified name lookup for class members](https://github.com/carbon-language/carbon-lang/pull/2287)

- [No unqualified lookup when defining outside a scope](/proposals/p2287.md#no-unqualified-lookup-when-defining-outside-a-scope)

## References

- [#257: Initialization of memory and variables](https://github.com/carbon-language/carbon-lang/pull/257)
- [#561: Basic classes: use cases, struct literals, struct types, and future work](https://github.com/carbon-language/carbon-lang/pull/561)
- [#722: Nominal classes and methods](https://github.com/carbon-language/carbon-lang/pull/722)
- [#777: Inheritance](https://github.com/carbon-language/carbon-lang/pull/777)
- [#875: Principle: Information accumulation](https://github.com/carbon-language/carbon-lang/pull/875)
- [#981: Implicit conversions for aggregates](https://github.com/carbon-language/carbon-lang/pull/981)
- [#1154: Destructors](https://github.com/carbon-language/carbon-lang/pull/1154)
- [#2107: Clarify rules around `Self` and `.Self`](https://github.com/carbon-language/carbon-lang/pull/2107)
- [#2287: Allow unqualified name lookup for class members](https://github.com/carbon-language/carbon-lang/pull/2287)
172 changes: 172 additions & 0 deletions proposals/p2287.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Allow unqualified name lookup

<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->

[Pull request](https://github.com/carbon-language/carbon-lang/pull/####)

<!-- toc -->

## Table of contents

- [Abstract](#abstract)
- [Problem](#problem)
- [Background](#background)
- [Proposal](#proposal)
- [Classes](#classes)
- [Interfaces](#interfaces)
- [Namespaces](#namespaces)
- [Open question](#open-question)
- [Implicit instance binding to `me`](#implicit-instance-binding-to-me)
- [Out-of-line definitions for impls](#out-of-line-definitions-for-impls)
- [Rationale](#rationale)
- [Alternatives considered](#alternatives-considered)
- [No unqualified lookup when defining outside a scope](#no-unqualified-lookup-when-defining-outside-a-scope)

<!-- tocstop -->

## Abstract

Allow unqualified name lookup in multiple situations:

- For classes and interfaces, whether inside the class scope or within an
out-of-line function definition.
- For namespaces, when the namespace is used in a declaration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This only changes the rules for name lookup, not for instance binding, so an unqualified name still cannot be used to refer to an instance member.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note about the open question to the proposal section.

## Problem

[Member access](/docs/design/expressions/member_access.md) defines certain
member access behaviors. However, it doesn't cover what happens if an
unqualified name lookup occurs within a class, particularly for an out-of-line
member function definition, or other situations.

## Background

The [member access design](/docs/design/expressions/member_access.md) and
[information accumulation principle](/docs/project/principles/information_accumulation.md)
affect this.

This will also work similarly to
[unqualified lookup within C++](https://en.cppreference.com/w/cpp/language/unqualified_lookup).

## Proposal

Allow unqualified name lookup which will use the appropriate scope.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a little funny to have the abstract be more concrete than the proposal section. Maybe swap the content of the two sections?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstract and the PR description are supposed to match. The text in the abstract is giving the amount of detail I believe to be correct here.

The text here is once sentence because of the paragraphs that follow it; that's my intent, to use sub-headers to explain what's only briefly noted in the abstract. It didn't seem long enough to be worth duplicating the abstract here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I think the abstract and proposal text makes sense to me at this point? the abstract is a summary, and the subsections here seem to directly correspond so I think this is OK now?


Implicit instance binding to `me` is not proposed; it is left as an
[open question](#implicit-instance-binding-to-me).

### Classes

This proposal updates [the class design](/docs/design/classes.md) to address
classes.

### Interfaces

```carbon
interface Vector {
fn Scale[me: Self](v: f64) -> Self;
// Default definition of `Invert` calls `Scale`.
default fn Invert[me: Self]() -> Self;
}

// `Self` is valid here because it's doing unqualified name lookup into
// `Vector`.
default fn Vector.Invert[me: Self]() -> Self {
// `Scale` is valid here because it does unqualified name lookup into
// `Vector`, then an instance binding with `me`.
return me.(Scale)(-1.0);
}
```

### Namespaces

More generally, this should also be true of other scopes used in declarations.
In particular, namespaces should also follow the same rule. However, since
[name lookup](/docs/design/name_lookup.md) has not been fleshed out, this won't
make much of an update to it.

An example for namespaces would be:

```carbon
namespace Foo;
var Foo.a: i32 = 0;

class Foo.B {}

// `B` and `a` are valid here because unqualified name lookup occurs within
// `Foo`.
fn Foo.C(B b) -> i32 {
return a;
}
```

## Open question

### Implicit instance binding to `me`

In C++, unqualified name lookup can implicitly do instance binding to `this`. In
other words, `this->Member()` and `Member()` behave similarly inside a method
definition.

In Carbon, the current design hasn't fleshed out whether `me` would behave
similarly. Most design documentation assumes it will not, but it hasn't been
directly considered in a proposal, and
[implicit scoped function parameters](https://github.com/carbon-language/carbon-lang/issues/1974)
might offer a way to make it work in a language-consistent manner.

This proposal takes no stance on unqualified name lookup resolving `me`: it is
not intended to change behavior from previous proposals.

### Out-of-line definitions for impls

Issue [#2377](https://github.com/carbon-language/carbon-lang/issues/2377) asks
how unqualified lookup should work for `impl`. The
[generics design](/docs/design/generics/details.md) also doesn't appear to give
syntax for out-of-line definitions of other impls.

## Rationale

- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
- Performing unqualified name lookup for class members should be fairly
unsurprising to readers, and should allow for more concise code when
working within a namespace.
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
- This behavior will be similar to how C++ works.

## Alternatives considered
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like us to consider the alternative of treating me as an ordinary parameter within method bodies, meaning that (modulo aliasing) it can be accessed only by explicitly naming it. In C++, I have often found it to be a readability obstacle that you can't easily find the places in a method body that access *this. Treating me as an explicitly-declared parameter gives us a clear path to avoiding that problem, so if we're not going to take it, I think we should explain why.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this changes the use of me at all? My impression was that this only applies to unqualified lookup that doesn't use the instance at all (or *this from C++), and only uses the type's scope.

Copy link
Contributor

@zygoloid zygoloid Oct 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a good idea for the proposal to say something about this to avoid confusion. My interpretation is that given:

class X {
  var v: i32;
  fn F[me: Self]() {
    v = 1;
    X.v = 1;
    me.v = 1;
    me.(v) = 1;
    me.(X.v) = 1;
  }
}
  • For v = 1;, unqualified lookup finds X.v, so this is equivalent to X.v = 1;, not me.v = 1;
  • For X.v = 1;, we are referring to an instance member without an instance, which is an error.
  • For me.v = 1;, we perform instance binding and assign to the member v of me.
  • For me.(v) = 1;, unqualified lookup finds X.v, so this is equivalent to me.(X.v) = 1;
  • For me.(X.v) = 1;, we perform instance binding and assign to the X member v of me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the missing part of zygoloid's example (see 981-985 in classes.md).

Note, my intent was just to fix out-of-date information and address what I thought would be a non-contentious change. I don't see me as either:

  1. I'm not familiar with the rationale behind disallowing me. references, nor could I find it considered anywhere.

  2. Requiring me. is consistent with C++ and seems more ergonomic to write (I don't think it's been a source of confusion in C++ code). Open design idea: implicit scoped function parameters #1974 also offers a way to make it work: me could be an implicit scoped parameter.

I'm not readily familiar with the argument for not supporting lookups with me. There may be refactoring reasons for disallowing it, but I would think those would be shared with unqualified name lookup in general.

Because the issue hasn't been addressed previously and shares problems with unqualified name lookup on classes, I think it's fair to ask here. That said, I also don't feel comfortable resolving this, so I've added notes for an open question.

Is the open question a reasonable answer for now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, I overlooked the distinction between name lookup and instance binding, so I thought this was proposing to e.g. make v = 1; be valid and equivalent to me.v = 1 in @zygoloid's example. I've added a suggestion to the abstract that would have cleared up my confusion. With that change or something like it, I don't think we even need the open question (but it doesn't hurt to have it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want some reference for this -- do you have an alternative reference that I can use in place of the open question?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would stress either way, the open question seems fine.


### No unqualified lookup when defining outside a scope

We could decide not to support unqualified lookup when defining something that
is presented within the top-level scope of the file.

Note this has subtle implications. If `Foo.C` in the namespace example is
considered to be outside the `Foo` scope for this purpose, it means the function
would need to look like:

```
fn Foo.C(Foo.B b) -> i32 {
return Foo.a;
}
```

It could also mean that, on a class, an inline declaration
`fn Foo() -> ClassMember` is valid, while an out-of-line definition
`fn Class.Foo() -> ClassMember` is not, requiring `Class.ClassMember`.

Advantages:

- Explicit in access.
- For example, name lookup results could be mildly confusing if both
`package.a` and `package.Foo.a` are defined but `package.Foo.a` is
hidden in code while `package.a` is easy to find. It's likely that
`package.Foo.a` would be considered unambiguous for unqualified name
lookup.

Disadvantages:

- Very verbose, and could prove un-ergonomic for developers.