diff --git a/docs/design/expressions/member_access.md b/docs/design/expressions/member_access.md index 1efb7d17fac4..a7ca333bef23 100644 --- a/docs/design/expressions/member_access.md +++ b/docs/design/expressions/member_access.md @@ -34,12 +34,14 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ## Overview -> **TODO:** > +> **TODO:** > [#3720: Member binding operators](/proposals/p003720-member-binding-operators.md) -> introduces an additional "member binding" step, redefines simple member access -> in terms of compound member access, and defines compound member access in -> terms of calls to user-implementable interface methods. This document must be -> updated to reflect those changes. +> and +> [#7697: Updates to member access](/proposals/p007697-updates-to-member-access.md) +> redefine simple member access in terms of compound member access, define +> instance binding in terms of calls to user-implementable interface methods, +> and add another member access form `x.impl(M)`. This document must be updated +> to reflect those changes. A _qualified name_ is a [word](../lexical_conventions/words.md) that is preceded by a period or a rightward arrow. The name is found within a contextually diff --git a/proposals/p007697-updates-to-member-access.md b/proposals/p007697-updates-to-member-access.md new file mode 100644 index 000000000000..d24a5f1b93e0 --- /dev/null +++ b/proposals/p007697-updates-to-member-access.md @@ -0,0 +1,607 @@ +# Updates to member access + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/7697) + + + +## Table of contents + +- [Abstract](#abstract) +- [Background](#background) +- [Problem](#problem) + - [Callables representing the result of `impl` lookup](#callables-representing-the-result-of-impl-lookup) + - [Accessing names with instance and non-instance overloads](#accessing-names-with-instance-and-non-instance-overloads) + - [Facets with members associated with different interfaces](#facets-with-members-associated-with-different-interfaces) + - [C++ pointer-to-member values](#c-pointer-to-member-values) + - [Calling an associated function](#calling-an-associated-function) + - [Properties](#properties) +- [Proposal](#proposal) +- [Details](#details) + - [Non-instance members](#non-instance-members) + - [Callables for member functions](#callables-for-member-functions) + - [Overloading](#overloading) + - [Explicit about instance binding](#explicit-about-instance-binding) + - [C++ pointer-to-member values](#c-pointer-to-member-values-1) + - [`typeof`](#typeof) + - [Associated function example](#associated-function-example) +- [Rationale](#rationale) +- [Alternatives considered](#alternatives-considered) + - [Different way to distinguish whether instance binding occurs](#different-way-to-distinguish-whether-instance-binding-occurs) + - [Non-instance members could implement the binding interfaces](#non-instance-members-could-implement-the-binding-interfaces) + - [Other member access operators](#other-member-access-operators) + - [Bind interfaces only used for compound member access](#bind-interfaces-only-used-for-compound-member-access) + + + +## Abstract + +Update the rules for member access: + +- Simple member access `a.b` + - If `a` names a scope, performs name lookup and optionally `impl` lookup. + - Otherwise, `a.b` is shorthand for `a.(typeof(a).b)` and always performs + instance binding. +- Compound member access `a.(m)` does optional `impl` lookup and always + performs instance binding. + - This is a change from only performing instance binding if `m` is an + instance member. +- New operation `a.impl(m)` is introduced. It always performs `impl` lookup, + and nothing else. +- The `BindToType` interface is removed. Only instance binding may be + customized (using the `BindToValue` and `BindToRef` interfaces). + +As a result, member access doesn't use whether the right operand is an instance +member anymore. Instead, instance binding is performed whenever it would be +plausible, and a new syntax is used to opt out. + +Associated functions of interfaces are also made callable when the `Self` type +can be deduced from the arguments. This also performs `impl` lookup. + +## Background + +- The + ["qualified names and member access" design document](/docs/design/expressions/member_access.md) + reflects the design up to and including: + - [Proposal #989: Member access expressions](https://github.com/carbon-language/carbon-lang/pull/989) + - [Proposal #2360: Types are values of type `type`](https://github.com/carbon-language/carbon-lang/pull/2360) + - [Proposal #3646: Tuples and tuple indexing](https://github.com/carbon-language/carbon-lang/pull/3646) +- [Proposal #3720: Member binding operators](https://github.com/carbon-language/carbon-lang/pull/3720) + updated the member access rules to add customization of how member access + worked by implementing binding interfaces. It defined some simple member + accesses as rewrites into compound member access. +- [Pull request #7557](https://github.com/carbon-language/carbon-lang/pull/7557) + attempted to update the + [member access design doc](/docs/design/expressions/member_access.md) to + reflect the changes in + [Proposal #3720](https://github.com/carbon-language/carbon-lang/pull/3720). +- [Leads issue #7606: Should associated function names be callable?](https://github.com/carbon-language/carbon-lang/issues/7606) + added another construct that performs `impl` lookup. +- There were several discussions to figure out how to resolve the ambiguous + points and resolve the problems we discovered: + - [#generics-and-templates discussion on 2026-08-20 on Discord](https://discord.com/channels/655572317891461132/941071822756143115/1540063686201311342) + - [discussion on 2026-08-24](https://docs.google.com/document/d/1mjllGO3ZCL4qGt9uJHUtcxKoHAGEY7Y999ie4EtBWB8/edit?tab=t.3ot8c9eu3e1h#heading=h.bpukwg8e3446) + - [#typesystem discussion starting 2026-08-27 on Discord](https://discord.com/channels/655572317891461132/708431657849585705/1542650207525929070) + - [discussion on 2026-08-28](https://docs.google.com/document/d/1mjllGO3ZCL4qGt9uJHUtcxKoHAGEY7Y999ie4EtBWB8/edit?pli=1&tab=t.3ot8c9eu3e1h#heading=h.s780u75i71d1) + - [discussion on 2026-08-31](https://docs.google.com/document/d/1mjllGO3ZCL4qGt9uJHUtcxKoHAGEY7Y999ie4EtBWB8/edit?pli=1&tab=t.3ot8c9eu3e1h#heading=h.4ij84uxqftu5) + - [discussion on 2026-09-10](https://docs.google.com/document/d/1mjllGO3ZCL4qGt9uJHUtcxKoHAGEY7Y999ie4EtBWB8/edit?tab=t.8973cke7ijhm#heading=h.gxlscluaucu0) + +## Problem + +[Proposal #3720: Member binding operators](https://github.com/carbon-language/carbon-lang/pull/3720) +introduced some problems discovered when trying to update the design documents +in [PR #7557](https://github.com/carbon-language/carbon-lang/pull/7557): + +- No clear story for what use cases are solved by `BindToType` and when that + customization would be needed. +- Simple member access `a.b` was defined as a rewrite to compound member + access `a.(M)`, but the definition of compound member access depended on + whether `M` was an instance member, and so would not always have the desired + behavior. +- Unclear story around how we support overloading between instance and + non-instance members. It seem to involve a default implementation of the + binding interfaces for all types for use by non-instance members. Could + non-instance members be repeatedly bound? + +We thought it should be more obvious in which cases instance binding would +occur. In this example, + +``` +class C { + extend base: (i32, i32); + static let template n: i32 = 0; +} + +var x: C = {.base = (1, 2)}; +``` + +Does `x.n` have the value `0` like `C.n` or `1` like `x.(0)`? The interpretation +depends on whether the binding interfaces are implemented for these types. In a +generic context, that could be unknown at checking time, as in this example: + +```carbon +class D(T: Core.Default) { + static let template x: T = T.Op(); +} + +fn F[T: type](d: D(T)) { + // Does the result here have value `T`, or is it the + // result of binding `T.Op()` to `d`? + d.x +} +``` + +This would lead to awkward constraints on types in order to get expected normal +behavior in generic code. + +The implementation of #3720 in the toolchain also looked to be expensive, with +broad blanket implementations of interfaces to get the expected default +behavior, leading to lots of `impl` lookups. As much as possible, builtin +`impl`s should be `final` or narrow. + +### Callables representing the result of `impl` lookup + +We want some way of creating callables from methods and member function from +interfaces, with the option of binding or not binding `self` for associated +methods. + +```carbon +interface I { + fn F(); + fn M(self); +} + +class C {} +impl C as I { ... } + +fn G(c: C) { + // Would like to make callables for: + // - impl lookup of `I.F` for `C` + // - impl lookup of `I.M` for `C` taking a `C` parameter for `self` + // - impl lookup of `I.M` for `C` where the `self` parameter is bound to `c`. +} +``` + +Before this proposal, the behavior of compound member access was different for +instance methods and non-instance member functions associated with an interface: + +- `C.(I.F)` would produce the result of looking up `I.F` for `C`. +- `c.(I.F)` was invalid, since `c` is not a type, and so can't implement `I`. +- `C.(I.M)` was invalid, since `I.M` is an instance member and can't perform + instance binding to `C`. +- `c.(I.M)` would perform `impl` lookup and then instance binding of `I.M` to + `c`. + +And there was no way, beyond writing a lambda, to get the result of `impl` +lookup of `I.M` in `C` without performing instance binding. + +### Accessing names with instance and non-instance overloads + +[Proposal #3720: Member binding operators](https://github.com/carbon-language/carbon-lang/pull/3720) +erased some of the differences between instance and non-instance members in +order to support names that had both as overloads, to pave the way for +overloading to be added to the language, as in (using the function overload +syntax from +[discussion on 2025-03-28](https://docs.google.com/document/d/1Iut5f2TQBrtBNIduF4vJYOKfw7MbS8xH_J01_Q4e6Rk/edit?resourcekey=0-mc_vh5UzrzXfU4kO-3tOjA&tab=t.0#heading=h.t6l733mu79i7)) + +```carbon +interface I { + overload F { + fn (self) -> f32; + fn (i32) -> bool; + } +} + +class C { + overload G { + fn (self) -> f32; + fn (i32) -> bool; + } +} +``` + +However, not all of the differences were eliminated, so it was unclear whether +the rewrite from simple to compound member access should use the type or value +of the left operand. + +### Facets with members associated with different interfaces + +We would like to support members of facets that are associated entities of +different interfaces, as in this example: + +```carbon +interface I { + fn F(); + fn M(self); +} + +interface J { + require impls I; + alias I_F = I.F; + alias I_M = I.M; +} + +fn G[T: J](x: T) { + // `T` is a facet of `J`, but the names `I_F` and `I_M` + // from `J` refer to members of `I`. Access to those + // members by way of `x` or `T` should work and use the + // implementation of `I` by `T`. +} +``` + +This means member access into facets still needs to perform `impl` lookup, even +though in many cases the facet has the implementation in its witness. + +### C++ pointer-to-member values + +[C++ pointer-to-member](https://en.cppreference.com/cpp/language/pointer) +values should be usable from Carbon once bound to an instance. + +```carbon +import Cpp inline ''' +struct A { + int m; + auto F() -> int; +}; + +int A::* p = &A::m; +int (A::* q)() = &A::F; +'''; + +fn G(ref a: Cpp.A) -> i32 { + // Equivalent to `a.*p + (a.*q)()` in C++. + // Evaluates to `a.m + a.F()`. + return a.(Cpp.p) + a.(Cpp.q)(); +} +``` + +### Calling an associated function + +Following +[Leads issue #7606: Should associated function names be callable?](https://github.com/carbon-language/carbon-lang/issues/7606), +associated method of an interface should be allowed, with the `self` parameter +and `Self` type determined by the first argument, similar to how +[proposal #7016](p007016-updating-self-syntax-and-adding-static-member-variables.md#calling-without-method-syntax) +added support for calling methods as ordinary functions with the `self` passed +as the first explicit argument in the explicit parameter list `(`...`)`: + +```carbon +interface Interface { + fn Method(self); +} + +class Class { + extend impl as Interface { fn Method(unused self) {} } +} + +fn Fn(value: Class) { + // Allowed as of proposal #7016: + (Class as Interface).Method(value); + // Leads decided should be allowed in #7606: + Interface.Method(value); +} +``` + +The specifics of the decision in #7606 allow non-method associated functions of +an interface, as long as the `Self` parameter may be deduced from its arguments. +This is desirable since otherwise this case would not have an ergonomic syntax. + +### Properties + +See the +[future work section on properties in proposal #3720](/proposals/p003720-member-binding-operators.md#future-properties). +For purposes of this proposal, we want it clear that the custom code for +producing values for a property would be invoked by instance binding, and so it +is important that instance binding happen when writing expressions that looked +like ordinary member access. Ideally we would also have another syntax available +to be able to talk about the property before instance binding. + +## Proposal + +We provisionally define `typeof(x)` to give the static type of the expression +`x` without any runtime evaluation of `x`. + +Compound member access `a.(m)` performs two steps: + +- If `m` is an associated entity, perform `impl` lookup with the `Self` type + set to the type of `a`. This must succeed and be valid. + - For an interface `I` and class `C`, both `C.(I.F)` and `I.(I.F)` will + fail due to `typeof(a) == type` not implementing `I`. +- Instance binding is performed, using either the `BindToValue` or `BindToRef` + interfaces implemented by `typeof(a)`. As in + [proposal #3720](https://github.com/carbon-language/carbon-lang/pull/3720), + the compiler provides `final` builtin implementations to provide the + previous instance binding behavior. + +Simple member access `a.b` depends on what kind of entity `a` is: + +- If `a` is a namespace or package, only name lookup for `b` is performed. +- If `a` names a non-type facet, then `b` is looked up in the type of `a` + (which by definition is a facet type such as an interface). If the lookup + finds an associated entity, then `impl` lookup is performed. This lookup + commonly can be satisfied by the facet `a`. This `impl` lookup is needed to + address the + ["facets with members associated with different interfaces" problem](#facets-with-members-associated-with-different-interfaces). +- If `a` names a facet type, then `a.b` performs name lookup for `b` in `a`. +- If `a` names another type (including any class), then `a.b` performs name + lookup for `b` in `a`. If the result of lookup is an associated entity, then + `impl` lookup is performed. +- Otherwise, `a.b` is performed in two steps: + - `m` is set to the result of evaluating `typeof(a).b`; + - Instance binding is performed to bind `a` to `m`. This is done + unconditionally, unlike prior to this proposal. + +In this last case: + +- `typeof(a)` will always be a facet type or other type, so `typeof(a).b` + will always be resolved using one of the above rules, and won't require + further rewrites. +- If `b` is an associated entity, `typeof(a).b` will perform `impl` lookup + using `typeof(a)`. + - As long as the result `m` is not itself an associated entity, `a.b` + will be equivalent to `a.(m)`. We don't say that `a.b` is rewritten to + `a.(typeof(a).b)` because we want a member access to perform at most + one `impl` lookup. + +Instead of writing `C.(I.F)` to perform `impl` lookup, we introduce new syntax +`C.impl(I.F)`. This always performs `impl` lookup with the the `Self` type equal +to `C` and nothing else. It is invalid unless `C` is known to implement `I` +(this check is delayed until the expression is no longer template dependent). +As a result, the left argument must always be a type or facet. + +In addition, `impl` lookup occurs when +[calling an associated function of an interface](#calling-an-associated-function). +An associated function of an interface `I` is callable, and in a call to it, the +`Self` parameter is treated as a generic parameter that can be deduced. After +`Self` is deduced, `impl` lookup is performed for `Self as I`, and the +corresponding function from the impl is called. Note that this is allowed for +any associated function for which `Self` can be deduced, not just for associated +methods. + +## Details + +Simple member access `a.b` requires knowing what kind of entity the first `a` +operand is. In generic code, it might not be known whether a symbolic value +represents a type or some other kind of value. In that case, though, simple +member access isn't useful since we don't know enough about `a` to perform name +lookup into it. + +### Non-instance members + +Non-instance members of types (including classes and interfaces) no longer +implement the binding interfaces, and so may not be used with instance binding. + +```carbon +interface I { + // Non-instance member function + fn F(); +} + +class C { + // Non-instance member function + fn G(); + + // Non-instance static data member + static var s: i32; + + extend impl as I; +} + +fn PreviouslyAllowedNowInvalid(x: C) { + // Previously allowed, but now invalid: + // ❌ x.F(); + // ❌ x.G(); + // ❌ x.s = 1; +} + +fn Instead(x: C) { + // Instead, these should be written: + C.F(); // ✅ + C.impl(I.F)(); // ✅ + typeof(x).F(); // ✅ + + C.G(); // ✅ + typeof(x).G(); // ✅ + + C.s = 1; // ✅ + typeof(x).s = 1; // ✅ +} +``` + +We require that the caller distinguish whether they are performing instance +binding, which means that changing a method to a non-instance member function +requires updating callers. + +### Callables for member functions + +Thanks to the new `a.impl(m)` syntax, we can now produce callables for all of +the cases in the +["callables representing the result of `impl` lookup" section](#callables-representing-the-result-of-impl-lookup): + +```carbon +interface I { + fn F(); + fn M(self); +} + +class C {} +impl C as I { ... } + +fn G(c: C) { + // impl lookup of `I.F` for `C`: `C.impl(I.F)` + C.impl(I.F)(); + // or: + typeof(c).impl(I.F)(); + + // impl lookup of `I.M` for `C` taking a `C` parameter for `self`: `C.impl(I.M)`. + // This may be called with `c` passed in for `self` using: + C.impl(I.M)(c); + // or: + c.(C.impl(I.M))(); + + // impl lookup of `I.M` for `C` where the `self` parameter is bound to `c`: + // `c.(I.M)` + c.(I.M)(); + + // Equivalent to `c.(I.M)()`: + I.M(c); +} +``` + +Note how this changes the meaning of compound member access from before this +proposal: + +- `C.(I.F)` used to produce the result of looking up `I.F` for `C`, but is no + longer valid since instance binding to `C` fails. The new syntax + `C.impl(I.F)` is used instead. +- `c.(I.F)` and `C.(I.M)` remain invalid. +- The common case of `c.(I.M)` retains its previous meaning. + +### Overloading + +The +[overloading problem](#accessing-names-with-instance-and-non-instance-overloads) +is addressed by not using anything about the second operand to decide whether to +perform instance binding or whether to use the value or type of the left operand +for `impl` lookup. The only fact about the right operand that is used in the +proposed rules is whether it names an associated entity. + +### Explicit about instance binding + +With this proposal, `a.(m)` always performs instance binding, and `a.b` performs +instance binding unless `a` is a kind of entity where we never perform instance +binding such as packages and namespaces. Cases where you want to avoid instance +binding now have a separate syntax (`typeof(a).impl(m)`). + +This means that generic code has a clear meaning, and transforming non-generic +code to be generic won't change behavior. + +For [properties](#properties), this means that the normal ways of accessing +members will perform the instance binding that triggers the evaluation of the +property, but there is an opt-out syntax (`typeof(a).m`) when that is not +desired. + +### C++ pointer-to-member values + +The solution to +[the "C++ pointer-to-member values" problem](#c-pointer-to-member-values) from +[proposal #3720](https://github.com/carbon-language/carbon-lang/pull/3720) +continues to work. + +### `typeof` + +`typeof(x)` has no runtime side effects, and produces a compile-time result. +This may involve compile-time evaluation, but all runtime effects from that +evaluation are discarded before code generation, as if the code is in an +`if (false)` block. For example: + +```carbon +musteval fn P(T: type) -> type { + return T*; +} + +fn F[template T: type](ref x: T) -> P(T) { + x += 1; + return &x; +} + +fn Call() { + var y: i32 = 0; + // Involves the compile-time evaluation of `P(i32)`, + // and forming a specific instance of `F`. However, + // `F(ref y)` is not called at runtime. + StaticAssert(typeof(F(ref y)) == i32*); + Assert(y == 0); +} +``` + +### Associated function example + +Here is an example from +[leads issue #7606](https://github.com/carbon-language/carbon-lang/issues/7606) +where we expect `Self` to be deduced and used for `impl` lookup when calling a +non-method associated function: + +```carbon +interface Printable { + // Print one `Self` object. + fn Print(self); + // Print a sequence of `Self` objects. + fn PrintSlice(s: slice(Self)); +} +impl Widget as Printable { ... } +fn PrintWidgets(s: slice(Widget)) { + // OK, deduces `Self` is `Widget`. Equivalent to + // `(Widget as Printable).PrintSlice(s)`. + Printable.PrintSlice(s); +} +``` + +Note that `Self` is in deducible position, but not directly the type of any +argument. + +## Rationale + +This proposal makes the behavior of member access more explicit, reducing +context sensitivity in accordance with +[the Carbon principle](/docs/project/principles/low_context_sensitivity.md). +Reducing ambiguity improves +[code readability](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write), +as does eliminating needing +[extra constraints for generic code](#explicit-about-instance-binding). + +## Alternatives considered + +### Different way to distinguish whether instance binding occurs + +Instead of using `a.impl(b)` to skip instance binding, other syntax ideas were +[considered on 2026-08-28](https://docs.google.com/document/d/1mjllGO3ZCL4qGt9uJHUtcxKoHAGEY7Y999ie4EtBWB8/edit?tab=t.3ot8c9eu3e1h#heading=h.s780u75i71d1) + +- `a.(b)` would not do instance binding, and `a.[b]` would. This made the more + common case of instance binding look more unusual, and didn't provide a + keyword in the rarer case that could be used to search the documentation or + the web to understand what it meant. +- `a.static(b)` was considered instead of `a.impl(b)`. `impl` was preferred + since it better conveyed that `impl` lookup is the only thing that happens + in that operation. + +### Non-instance members could implement the binding interfaces + +In +[discussion on 2026-09-10](https://docs.google.com/document/d/1mjllGO3ZCL4qGt9uJHUtcxKoHAGEY7Y999ie4EtBWB8/edit?tab=t.8973cke7ijhm#heading=h.gxlscluaucu0), +we considered letting [non-instance members](#non-instance-members) implement +the binding interfaces. There were a few variations: + +- Instance binding to a non-instance member could do nothing. This would allow + repeated bindings, which was undesirable on its own, since none of them + would be doing anything, obscuring the meaning of the code. This was also + inconsistent with instance members where repeated binding is forbidden. +- Instance binding could do nothing except change the type to something that + wasn't bindable again, but otherwise operated similarly. This would have to + be restricted to non-instance member functions, so that we could forward + just the call operator, not an unbounded set of operations. This is + something we would consider in the future to match C++ and allow evolution + of methods into non-instance functions, but creates additional complexity + and inconsistency with other non-instance members like static data members. + +Since the main point of customization for types is whether they implement the +binding interfaces, we have less context than C++ about what syntax was used to +arrive at an instance binding. It wasn't clear how to make a rule that allowed +non-instance accesses like C++ without allowing code we wanted to forbid like +`i32.(bool.(5))`. + +### Other member access operators + +We considered introducing a `::` that primarily did qualified name lookup. This +didn't address the root causes of the problem, though, which was the varying +behavior after name lookup completed. + +### Bind interfaces only used for compound member access + +The bind interfaces needed to be used with simple member access as well, +otherwise properties would appear different than other members.