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
224 changes: 180 additions & 44 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 @@ -75,6 +76,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [No `static` variables](#no-static-variables)
- [Computed properties](#computed-properties)
- [Interfaces implemented for data classes](#interfaces-implemented-for-data-classes)
- [Alternatives considered](#alternatives-considered)
- [References](#references)

<!-- tocstop -->
Expand Down Expand Up @@ -913,71 +915,124 @@ 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: an instance is required.
return size * size;
// ❌ Error: an instance is required.
return Square.size * Square.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 Point.Angle[me: Self]() -> f32 {
// ✅ Allowed: `Point` type is complete.
// Function is checked immediately.
return Math.ATan2(me.y, me.x);
fn Create(size: f32) -> Square;

var size: f32;
}
```

**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).
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 Iterate() -> Iterator;

class Iterator;

// ✅ OK: A forward declaration is sufficient to produce a pointer.
fn Iterate() -> Iterator*;
// ❌ Error: A forward declaration cannot be used to return a value.
fn Iterate() -> Iterator;
chandlerc marked this conversation as resolved.
Show resolved Hide resolved

class Iterator {
...
}

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

An out-of-line function definition's full signature is evaluated as if in-scope.
zygoloid marked this conversation as resolved.
Show resolved Hide resolved
For example:

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

### Nominal data classes

Expand Down Expand Up @@ -2100,12 +2155,93 @@ that "`T` is comparable to `U` if they have the same field names in the same
order, and for every field `x`, the type of `T.x` implements `ComparableTo` for
the type of `U.x`."

## Alternatives considered

chandlerc marked this conversation as resolved.
Show resolved Hide resolved
- [#257: Initialization of memory and variables](https://github.com/carbon-language/carbon-lang/pull/257)

- [Require compile-time-proven initialization](/proposals/p0257.md#require-compile-time-proven-initialization)
- [C/C++ uninitialized](/proposals/p0257.md#cc-uninitialized)
- [Allow passing unformed objects to parameters or returning them?](/proposals/p0257.md#allow-passing-unformed-objects-to-parameters-or-returning-them)
- [Allow assigning an unformed object to another unformed object?](/proposals/p0257.md#allow-assigning-an-unformed-object-to-another-unformed-object)
- [Fully destructive move (Rust)](/proposals/p0257.md#fully-destructive-move-rust)
- [Completely non-destructive move (C++)](/proposals/p0257.md#completely-non-destructive-move-c)
- [Named return variable in place of a return type](/proposals/p0257.md#named-return-variable-in-place-of-a-return-type)
- [Allow unformed members](/proposals/p0257.md#allow-unformed-members)

- [#561: Basic classes: use cases, struct literals, struct types, and future work](https://github.com/carbon-language/carbon-lang/pull/561)

- [Early proposal #98](https://github.com/carbon-language/carbon-lang/pull/98)
- [Interfaces implemented for anonymous data classes](/proposals/p0561.md#interfaces-implemented-for-anonymous-data-classes)
- [Access control](/proposals/p0561.md#access-control)
- [Introducer for structural data class types](/proposals/p0561.md#introducer-for-structural-data-class-types)
- [Terminology](/proposals/p0561.md#terminology)

- [#722: Nominal classes and methods](https://github.com/carbon-language/carbon-lang/pull/722)

- [Method syntax](/proposals/p0722.md#method-syntax)
- [Marking mutating methods at the call site](/proposals/p0722.md#marking-mutating-methods-at-the-call-site)
- [Differences between functions and methods](/proposals/p0722.md#differences-between-functions-and-methods)
- [Specifying linkage as part of the access modifier](/proposals/p0722.md#specifying-linkage-as-part-of-the-access-modifier)
- [Nominal data class](/proposals/p0722.md#nominal-data-class)
- [Let constants](/proposals/p0722.md#let-constants)

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

- [Classes are final by default](/proposals/p0777.md#classes-are-final-by-default)
- [Allow keywords to be written when they would have no effect](/proposals/p0777.md#allow-keywords-to-be-written-when-they-would-have-no-effect)
- [Different virtual override keywords](/proposals/p0777.md#different-virtual-override-keywords)
- [Different virtual override keyword placement](/proposals/p0777.md#different-virtual-override-keyword-placement)
- [Final methods](/proposals/p0777.md#final-methods)
- [Constructors](/proposals/p0777.md#constructors)
- [Implicit abstract classes](/proposals/p0777.md#implicit-abstract-classes)
- [No extensible objects with non-virtual destructors](/proposals/p0777.md#no-extensible-objects-with-non-virtual-destructors)
- [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)
- [Different field orders are incompatible](/proposals/p0981.md#different-field-orders-are-incompatible)
- [Explicit instead of implicit conversions](/proposals/p0981.md#explicit-instead-of-implicit-conversions)

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

- [Types implement destructor interface](/proposals/p1154.md#types-implement-destructor-interface)
- [Prevent virtual function calls in destructors](/proposals/p1154.md#prevent-virtual-function-calls-in-destructors)
- [Allow functions to act as destructors](/proposals/p1154.md#allow-functions-to-act-as-destructors)
- [Allow private destructors](/proposals/p1154.md#allow-private-destructors)
- [Allow multiple conditional destructors](/proposals/p1154.md#allow-multiple-conditional-destructors)
- [Type-of-type naming](/proposals/p1154.md#type-of-type-naming)
- [Other approaches to extensible classes without vtables](/proposals/p1154.md#other-approaches-to-extensible-classes-without-vtables)

- [#2107: Clarify rules around `Self` and `.Self`](https://github.com/carbon-language/carbon-lang/pull/2107)

- [`Self` not a keyword](/proposals/p2107.md#self-not-a-keyword)
- [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 wor](https://github.com/carbon-language/carbon-lang/pull/561)
- [#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)
116 changes: 116 additions & 0 deletions proposals/p2287.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Allow unqualified name lookup for class members

<!--
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)
- [Namespaces](#namespaces)
- [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 for class members, whether inside the class scope
or within an out-of-line function definition.

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.

## 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 for class members.

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

### 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;
}
```

## 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 not support unqualified lookup when defining something that is
zygoloid marked this conversation as resolved.
Show resolved Hide resolved
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.