Skip to content

Commit

Permalink
Allow unqualified name lookup for class members (#2287)
Browse files Browse the repository at this point in the history
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.

Co-authored-by: Richard Smith <[email protected]>
  • Loading branch information
jonmeow and zygoloid authored Nov 9, 2022
1 parent 4885a2b commit 14cc716
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 43 deletions.
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);
}
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.

## 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.

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

### 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.

0 comments on commit 14cc716

Please sign in to comment.