Updates to member access
Table of contents
Abstract
Update the rules for member access:
- Simple member access
a.b- If
anames a scope, performs name lookup and optionallyimpllookup. - Otherwise,
a.bis shorthand fora.(typeof(a).b)and always performs instance binding.
- If
- Compound member access
a.(m)does optionalimpllookup and always performs instance binding.- This is a change from only performing instance binding if
mis an instance member.
- This is a change from only performing instance binding if
- New operation
a.impl(m)is introduced. It always performsimpllookup, and nothing else. - The
BindToTypeinterface is removed. Only instance binding may be customized (using theBindToValueandBindToRefinterfaces).
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 reflects the design up to and including:
- Proposal #3720: Member binding operators 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 attempted to update the member access design doc to reflect the changes in Proposal #3720.
- Leads issue #7606: Should associated function names be callable? added another construct that performs
impllookup. - There were several discussions to figure out how to resolve the ambiguous points and resolve the problems we discovered:
Problem
Proposal #3720: Member binding operators introduced some problems discovered when trying to update the design documents in PR #7557:
- No clear story for what use cases are solved by
BindToTypeand when that customization would be needed. - Simple member access
a.bwas defined as a rewrite to compound member accessa.(M), but the definition of compound member access depended on whetherMwas 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:
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 impls 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.
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 upI.FforC.c.(I.F)was invalid, sincecis not a type, and so can’t implementI.C.(I.M)was invalid, sinceI.Mis an instance member and can’t perform instance binding toC.c.(I.M)would performimpllookup and then instance binding ofI.Mtoc.
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 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)
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:
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 values should be usable from Carbon once bound to an instance.
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?, 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 added support for calling methods as ordinary functions with the self passed as the first explicit argument in the explicit parameter list (…):
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. 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
mis an associated entity, performimpllookup with theSelftype set to the type ofa. This must succeed and be valid.- For an interface
Iand classC, bothC.(I.F)andI.(I.F)will fail due totypeof(a) == typenot implementingI.
- For an interface
- Instance binding is performed, using either the
BindToValueorBindToRefinterfaces implemented bytypeof(a). As in proposal #3720, the compiler providesfinalbuiltin implementations to provide the previous instance binding behavior.
Simple member access a.b depends on what kind of entity a is:
- If
ais a namespace or package, only name lookup forbis performed. - If
anames a non-type facet, thenbis looked up in the type ofa(which by definition is a facet type such as an interface). If the lookup finds an associated entity, thenimpllookup is performed. This lookup commonly can be satisfied by the faceta. Thisimpllookup is needed to address the “facets with members associated with different interfaces” problem. - If
anames a facet type, thena.bperforms name lookup forbina. - If
anames another type (including any class), thena.bperforms name lookup forbina. If the result of lookup is an associated entity, thenimpllookup is performed. - Otherwise,
a.bis performed in two steps:mis set to the result of evaluatingtypeof(a).b;- Instance binding is performed to bind
atom. This is done unconditionally, unlike prior to this proposal.
In this last case:
typeof(a)will always be a facet type or other type, sotypeof(a).bwill always be resolved using one of the above rules, and won’t require further rewrites.- If
bis an associated entity,typeof(a).bwill performimpllookup usingtypeof(a).- As long as the result
mis not itself an associated entity,a.bwill be equivalent toa.(m). We don’t say thata.bis rewritten toa.(typeof(a).b)because we want a member access to perform at most oneimpllookup.
- As long as the result
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. 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.
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:
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 upI.FforC, but is no longer valid since instance binding toCfails. The new syntaxC.impl(I.F)is used instead.c.(I.F)andC.(I.M)remain invalid.- The common case of
c.(I.M)retains its previous meaning.
Overloading
The overloading problem 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, 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 from proposal #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:
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 where we expect Self to be deduced and used for impl lookup when calling a non-method associated function:
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. Reducing ambiguity improves code readability, as does eliminating needing extra constraints for generic code.
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
a.(b)would not do instance binding, anda.[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 ofa.impl(b).implwas preferred since it better conveyed thatimpllookup is the only thing that happens in that operation.
Non-instance members could implement the binding interfaces
In discussion on 2026-09-10, we considered letting 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.