From 14cc716ddc3a90a1b26f5a6c15a1b8fd538a37bd Mon Sep 17 00:00:00 2001 From: Jon Ross-Perkins Date: Wed, 9 Nov 2022 14:34:30 -0800 Subject: [PATCH] Allow unqualified name lookup for class members (#2287) 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 --- docs/design/classes.md | 154 +++++++++++++++++++++++++----------- proposals/p2287.md | 172 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 43 deletions(-) create mode 100644 proposals/p2287.md diff --git a/docs/design/classes.md b/docs/design/classes.md index 9c1fc4ea60b6..80c548b7dd51 100644 --- a/docs/design/classes.md +++ b/docs/design/classes.md @@ -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) @@ -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.Angle[me: Self]() -> f32 { - // ✅ Allowed: `Point` type is complete. - // Function is checked immediately. - return Math.ATan2(me.y, me.x); +fn Point.Create(x: f32, y: f32) -> Point { + return {.x = x, .y = y}; } ``` -**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). +#### 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; + + class Iterator { + ... + } + + // ✅ OK: The definition of Iterator is now available. + fn Iterate() -> Iterator; +} +``` + +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 @@ -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) @@ -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) diff --git a/proposals/p2287.md b/proposals/p2287.md new file mode 100644 index 000000000000..aaaa21184e39 --- /dev/null +++ b/proposals/p2287.md @@ -0,0 +1,172 @@ +# Allow unqualified name lookup + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/####) + + + +## 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) + + + +## 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.