mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 19:00:11 +01:00
Member access expressions (#989)
Support for member access expressions with syntax `container.member`, covering cases such as: * `object.field` * `object.method(args)` * `package.namespace.class.member` * `object.(interface.member)` * `object.(class.member)` ... and so on. Includes the rule for template name lookup as decided in #949. Co-authored-by: josh11b <josh11b@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
josh11b
parent
7cce1bd124
commit
18b423dc5f
@@ -12,6 +12,9 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Precedence](#precedence)
|
||||
- [Names](#names)
|
||||
- [Unqualified names](#unqualified-names)
|
||||
- [Qualified names and member access](#qualified-names-and-member-access)
|
||||
- [Operators](#operators)
|
||||
- [Conversions and casts](#conversions-and-casts)
|
||||
- [`if` expressions](#if-expressions)
|
||||
@@ -54,6 +57,13 @@ graph BT
|
||||
braces["{...}"]
|
||||
click braces "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/classes.md#literals"
|
||||
|
||||
unqualifiedName["x"]
|
||||
click unqualifiedName "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/README.md#unqualified-names"
|
||||
|
||||
memberAccess>"x.y<br>
|
||||
x.(...)"]
|
||||
click memberAccess "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/member_access.md"
|
||||
|
||||
as["x as T"]
|
||||
click as "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/implicit_conversions.md"
|
||||
|
||||
@@ -79,7 +89,8 @@ graph BT
|
||||
|
||||
expressionEnd["x;"]
|
||||
|
||||
as & not --> parens & braces
|
||||
memberAccess --> parens & braces & unqualifiedName
|
||||
as & not --> memberAccess
|
||||
comparison --> as
|
||||
and & or --> comparison & not
|
||||
if & expressionEnd --> and & or
|
||||
@@ -113,6 +124,79 @@ The diagram's attributes are:
|
||||
- For example, `+` and `-` are left-associative and in the same precedence
|
||||
group, so `a + b + c - d` is treated as `((a + b) + c) - d`.
|
||||
|
||||
## Names
|
||||
|
||||
### Unqualified names
|
||||
|
||||
An _unqualified name_ is a [word](../lexical_conventions/words.md) that is not a
|
||||
keyword and is not preceded by a period (`.`).
|
||||
|
||||
**TODO:** Name lookup rules for unqualified names.
|
||||
|
||||
### Qualified names and member access
|
||||
|
||||
A _qualified name_ is a word that is prefixed by a period. Qualified names
|
||||
appear in the following contexts:
|
||||
|
||||
- [Designators](/docs/design/classes.md#literals): `.` _word_
|
||||
- [Direct member access expressions](member_access.md): _expression_ `.`
|
||||
_word_
|
||||
|
||||
```
|
||||
var x: auto = {.hello = 1, .world = 2};
|
||||
^^^^^ ^^^^^ qualified name
|
||||
^^^^^^ ^^^^^^ designator
|
||||
|
||||
x.hello = x.world;
|
||||
^^^^^ ^^^^^ qualified name
|
||||
^^^^^^^ ^^^^^^^ member access expression
|
||||
```
|
||||
|
||||
Qualified names refer to members of an entity determined by the context in which
|
||||
the expression appears. For a member access, the entity is named by the
|
||||
expression preceding the period. In a struct literal, the entity is the struct
|
||||
type. For example:
|
||||
|
||||
```
|
||||
package Foo api;
|
||||
namespace N;
|
||||
fn N.F() {}
|
||||
|
||||
fn G() {
|
||||
// Same as `(Foo.N).F()`.
|
||||
// `Foo.N` names namespace `N` in package `Foo`.
|
||||
// `(Foo.N).F` names function `F` in namespace `N`.
|
||||
Foo.N.F();
|
||||
}
|
||||
|
||||
// `.n` refers to the member `n` of `{.n: i32}`.
|
||||
fn H(a: {.n: i32}) -> i32 {
|
||||
// `a.n` is resolved to the member `{.n: i32}.n`,
|
||||
// and names the corresponding subobject of `a`.
|
||||
return a.n;
|
||||
}
|
||||
|
||||
fn J() {
|
||||
// `.n` refers to the member `n of `{.n: i32}`.
|
||||
H({.n = 5 as i32});
|
||||
}
|
||||
```
|
||||
|
||||
Member access expressions associate left-to-right. If the member name is more
|
||||
complex than a single _word_, an indirect member access expression can be used,
|
||||
with parentheses around the member name:
|
||||
|
||||
- _expression_ `.` `(` _member-access-expression_ `)`
|
||||
|
||||
```
|
||||
interface I { fn F[me: Self](); }
|
||||
class X {}
|
||||
external impl X as I { fn F[me: Self]() {} }
|
||||
|
||||
// `x.I.F()` would mean `(x.I).F()`.
|
||||
fn Q(x: X) { x.(I.F)(); }
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
Most expressions are modeled as operators:
|
||||
|
||||
@@ -0,0 +1,653 @@
|
||||
# Qualified names and member access
|
||||
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Member resolution](#member-resolution)
|
||||
- [Package and namespace members](#package-and-namespace-members)
|
||||
- [Lookup within values](#lookup-within-values)
|
||||
- [Templates and generics](#templates-and-generics)
|
||||
- [Lookup ambiguity](#lookup-ambiguity)
|
||||
- [`impl` lookup](#impl-lookup)
|
||||
- [Instance binding](#instance-binding)
|
||||
- [Non-instance members](#non-instance-members)
|
||||
- [Non-vacuous member access restriction](#non-vacuous-member-access-restriction)
|
||||
- [Precedence and associativity](#precedence-and-associativity)
|
||||
- [Alternatives considered](#alternatives-considered)
|
||||
- [References](#references)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Overview
|
||||
|
||||
A _qualified name_ is a [word](../lexical_conventions/words.md) that is preceded
|
||||
by a period. The name is found within a contextually determined entity:
|
||||
|
||||
- In a member access expression, this is the entity preceding the period.
|
||||
- For a designator in a struct literal, the name is introduced as a member of
|
||||
the struct type.
|
||||
|
||||
A _member access expression_ allows a member of a value, type, interface,
|
||||
namespace, and so on to be accessed by specifying a qualified name for the
|
||||
member.
|
||||
|
||||
A member access expression is either a _direct_ member access expression of the
|
||||
form:
|
||||
|
||||
- _member-access-expression_ ::= _expression_ `.` _word_
|
||||
|
||||
or an _indirect_ member access of the form:
|
||||
|
||||
- _member-access-expression_ ::= _expression_ `.` `(` _expression_ `)`
|
||||
|
||||
For example:
|
||||
|
||||
```carbon
|
||||
package Widgets api;
|
||||
interface Widget {
|
||||
fn Grow[addr me: Self*](factor: f64);
|
||||
}
|
||||
class Cog {
|
||||
var size: i32;
|
||||
fn Make(size: i32) -> Self;
|
||||
impl as Widgets.Widget;
|
||||
}
|
||||
|
||||
fn GrowSomeCogs() {
|
||||
var cog1: Cog = Cog.Make(1);
|
||||
var cog2: Cog = cog1.Make(2);
|
||||
let cog1_size: i32 = cog1.size;
|
||||
cog1.Grow(1.5);
|
||||
cog2.(Cog.Grow)(cog1_size as f64);
|
||||
cog1.(Widget.Grow)(1.1);
|
||||
cog2.(Widgets.Cog.(Widgets.Widget.Grow))(1.9);
|
||||
}
|
||||
```
|
||||
|
||||
A member access expression is processed using the following steps:
|
||||
|
||||
- First, the word or parenthesized expression to the right of the `.` is
|
||||
[resolved](#member-resolution) to a specific member entity, called `M` in
|
||||
this document.
|
||||
- Then, if necessary, [`impl` lookup](#impl-lookup) is performed to map from a
|
||||
member of an interface to a member of the relevant `impl`, potentially
|
||||
updating `M`.
|
||||
- Then, if necessary, [instance binding](#instance-binding) is performed to
|
||||
locate the member subobject corresponding to a field name or to build a
|
||||
bound method object, producing the result of the member access expression.
|
||||
- If [instance binding is not performed](#non-instance-members), the result is
|
||||
`M`.
|
||||
|
||||
## Member resolution
|
||||
|
||||
The process of _member resolution_ determines which member `M` a member access
|
||||
expression is referring to.
|
||||
|
||||
### Package and namespace members
|
||||
|
||||
If the first operand is a package or namespace name, the member access must be
|
||||
direct. The _word_ must name a member of that package or namespace, and the
|
||||
result is the package or namespace member with that name.
|
||||
|
||||
An expression that names a package or namespace can only be used as the first
|
||||
operand of a member access or as the target of an `alias` declaration.
|
||||
|
||||
```
|
||||
namespace MyNamespace;
|
||||
fn MyNamespace.MyFunction() {}
|
||||
|
||||
// ✅ OK, can alias a namespace.
|
||||
alias MyNS = MyNamespace;
|
||||
fn CallMyFunction() { MyNS.MyFunction(); }
|
||||
|
||||
// ❌ Error: a namespace is not a value.
|
||||
let MyNS2:! auto = MyNamespace;
|
||||
|
||||
fn CallMyFunction2() {
|
||||
// ❌ Error: cannot perform indirect member access into a namespace.
|
||||
MyNamespace.(MyNamespace.MyFunction)();
|
||||
}
|
||||
```
|
||||
|
||||
### Lookup within values
|
||||
|
||||
When the first operand is not a package or namespace name, there are three
|
||||
remaining cases we wish to support:
|
||||
|
||||
- The first operand is a value, and lookup should consider members of the
|
||||
value's type.
|
||||
- The first operand is a type, and lookup should consider members of that
|
||||
type. For example, `i32.Least` should find the member constant `Least` of
|
||||
the type `i32`.
|
||||
- The first operand is a type-of-type, and lookup should consider members of
|
||||
that type-of-type. For example, `Addable.Add` should find the member
|
||||
function `Add` of the interface `Addable`. Because a type-of-type is a type,
|
||||
this is a special case of the previous bullet.
|
||||
|
||||
Note that because a type is a value, and a type-of-type is a type, these cases
|
||||
are overlapping and not entirely separable.
|
||||
|
||||
If any of the above lookups ever looks for members of a type parameter, it
|
||||
should consider members of the type-of-type, treating the type parameter as an
|
||||
archetype.
|
||||
|
||||
**Note:** If lookup is performed into a type that involves a template parameter,
|
||||
the lookup will be performed both in the context of the template definition and
|
||||
in the context of the template instantiation, as described in
|
||||
[templates and generics](#templates-and-generics).
|
||||
|
||||
For a direct member access, the word is looked up in the following types:
|
||||
|
||||
- If the first operand can be evaluated and evaluates to a type, that type.
|
||||
- If the type of the first operand can be evaluated, that type.
|
||||
- If the type of the first operand is a generic type parameter, and the type
|
||||
of that generic type parameter can be evaluated, that type-of-type.
|
||||
|
||||
The results of these lookups are [combined](#lookup-ambiguity).
|
||||
|
||||
For an indirect member access, the second operand is evaluated as a constant to
|
||||
determine the member being accessed. The evaluation is required to succeed and
|
||||
to result in a member of a type or interface.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
interface Printable {
|
||||
fn Print[me: Self]();
|
||||
}
|
||||
external impl i32 as Printable;
|
||||
class Point {
|
||||
var x: i32;
|
||||
var y: i32;
|
||||
// Internal impl injects the name `Print` into class `Point`.
|
||||
impl as Printable;
|
||||
}
|
||||
|
||||
fn PrintPointTwice() {
|
||||
var p: Point = {.x = 0, .y = 0};
|
||||
|
||||
// ✅ OK, `x` found in type of `p`, namely `Point`.
|
||||
p.x = 1;
|
||||
// ✅ OK, `y` found in the type `Point`.
|
||||
p.(Point.y) = 1;
|
||||
|
||||
// ✅ OK, `Print` found in type of `p`, namely `Point`.
|
||||
p.Print();
|
||||
// ✅ OK, `Print` found in the type `Printable`.
|
||||
p.(Printable.Print)();
|
||||
}
|
||||
fn GenericPrint[T:! Printable](a: T) {
|
||||
// ✅ OK, type of `a` is the type parameter `T`;
|
||||
// `Print` found in the type of `T`, namely `Printable`.
|
||||
a.Print();
|
||||
}
|
||||
fn CallGenericPrint(p: Point) {
|
||||
GenericPrint(p);
|
||||
}
|
||||
```
|
||||
|
||||
#### Templates and generics
|
||||
|
||||
If the value or type of the first operand depends on a template or generic
|
||||
parameter, the lookup is performed from a context where the value of that
|
||||
parameter is unknown. Evaluation of an expression involving the parameter may
|
||||
still succeed, but will result in a symbolic value involving that parameter.
|
||||
|
||||
```
|
||||
class GenericWrapper(T:! Type) {
|
||||
var field: T;
|
||||
}
|
||||
fn F[T:! Type](x: GenericWrapper(T)) -> T {
|
||||
// ✅ OK, finds `GenericWrapper(T).field`.
|
||||
return x.field;
|
||||
}
|
||||
|
||||
class TemplateWrapper(template T:! Type) {
|
||||
var field: T;
|
||||
}
|
||||
fn G[template T:! Type](x: TemplateWrapper(T)) -> T {
|
||||
// 🤷 Not yet decided.
|
||||
return x.field;
|
||||
}
|
||||
```
|
||||
|
||||
> **TODO:** The behavior of `G` above is not yet fully decided. If class
|
||||
> templates can be specialized, then we cannot know the members of
|
||||
> `TemplateWrapper(T)` without knowing `T`, so this first lookup will find
|
||||
> nothing. In any case, as described below, the lookup will be performed again
|
||||
> when `T` is known.
|
||||
|
||||
If the value or type depends on any template parameters, the lookup is redone
|
||||
from a context where the values of those parameters are known, but where the
|
||||
values of any generic parameters are still unknown. The lookup results from
|
||||
these two contexts are [combined](#lookup-ambiguity).
|
||||
|
||||
**Note:** All lookups are done from a context where the values of any generic
|
||||
parameters that are in scope are unknown. Unlike for a template parameter, the
|
||||
actual value of a generic parameter never affects the result of member
|
||||
resolution.
|
||||
|
||||
```carbon
|
||||
class Cowboy { fn Draw[me: Self](); }
|
||||
interface Renderable {
|
||||
fn Draw[me: Self]();
|
||||
}
|
||||
external impl Cowboy as Renderable { fn Draw[me: Self](); }
|
||||
fn DrawDirect(c: Cowboy) { c.Draw(); }
|
||||
fn DrawGeneric[T:! Renderable](c: T) { c.Draw(); }
|
||||
fn DrawTemplate[template T:! Renderable](c: T) { c.Draw(); }
|
||||
|
||||
fn Draw(c: Cowboy) {
|
||||
// ✅ Calls member of `Cowboy`.
|
||||
DrawDirect(c);
|
||||
// ✅ Calls member of `impl Cowboy as Renderable`.
|
||||
DrawGeneric(c);
|
||||
// ❌ Error: ambiguous.
|
||||
DrawTemplate(c);
|
||||
}
|
||||
|
||||
class RoundWidget {
|
||||
external impl as Renderable {
|
||||
fn Draw[me: Self]();
|
||||
}
|
||||
alias Draw = Renderable.Draw;
|
||||
}
|
||||
|
||||
class SquareWidget {
|
||||
fn Draw[me: Self]() {}
|
||||
external impl as Renderable {
|
||||
alias Draw = Self.Draw;
|
||||
}
|
||||
}
|
||||
|
||||
fn DrawWidget(r: RoundWidget, s: SquareWidget) {
|
||||
// ✅ OK, lookup in type and lookup in type-of-type find the same entity.
|
||||
DrawTemplate(r);
|
||||
|
||||
// ✅ OK, lookup in type and lookup in type-of-type find the same entity.
|
||||
DrawTemplate(s);
|
||||
|
||||
// ✅ OK, found in type.
|
||||
r.Draw();
|
||||
s.Draw();
|
||||
}
|
||||
```
|
||||
|
||||
#### Lookup ambiguity
|
||||
|
||||
Multiple lookups can be performed when resolving a member access expression. If
|
||||
more than one member is found, after performing [`impl` lookup](#impl-lookup) if
|
||||
necessary, the lookup is ambiguous, and the program is invalid. Similarly, if no
|
||||
members are found, the program is invalid. Otherwise, the result of combining
|
||||
the lookup results is the unique member that was found.
|
||||
|
||||
## `impl` lookup
|
||||
|
||||
When the second operand of a member access expression resolves to a member of an
|
||||
interface `I`, and the first operand is a value other than a type-of-type,
|
||||
_`impl` lookup_ is performed to map the member of the interface to the
|
||||
corresponding member of the relevant `impl`. The member of the `impl` replaces
|
||||
the member of the interface in all further processing of the member access
|
||||
expression.
|
||||
|
||||
```carbon
|
||||
interface Addable {
|
||||
// #1
|
||||
fn Add[me: Self](other: Self) -> Self;
|
||||
// #2
|
||||
default fn Sum[Seq:! Iterable where .ValueType = Self](seq: Seq) -> Self {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
class Integer {
|
||||
impl as Addable {
|
||||
// #3
|
||||
fn Add[me: Self](other: Self) -> Self;
|
||||
// #4, generated from default implementation for #2.
|
||||
// fn Sum[...](...);
|
||||
}
|
||||
}
|
||||
|
||||
fn SumIntegers(v: Vector(Integer)) -> Integer {
|
||||
// Member resolution resolves the name `Sum` to #2.
|
||||
// `impl` lookup then locates the `impl Integer as Addable`,
|
||||
// and determines that the member access refers to #4,
|
||||
// which is then called.
|
||||
return Integer.Sum(v);
|
||||
}
|
||||
|
||||
fn AddTwoIntegers(a: Integer, b: Integer) -> Integer {
|
||||
// Member resolution resolves the name `Add` to #1.
|
||||
// `impl` lookup then locates the `impl Integer as Addable`,
|
||||
// and determines that the member access refers to #3.
|
||||
// Finally, instance binding will be performed as described later.
|
||||
// This can be written more verbosely and explicitly as any of:
|
||||
// - `return a.(Integer.Add)(b);`
|
||||
// - `return a.(Addable.Add)(b);`
|
||||
// - `return a.(Integer.(Addable.Add))(b);`
|
||||
return a.Add(b);
|
||||
}
|
||||
```
|
||||
|
||||
The type `T` that is expected to implement `I` depends on the first operand of
|
||||
the member access expression, `V`:
|
||||
|
||||
- If `V` can be evaluated and evaluates to a type, then `T` is `V`.
|
||||
```carbon
|
||||
// `V` is `Integer`. `T` is `V`, which is `Integer`.
|
||||
// Alias refers to #2.
|
||||
alias AddIntegers = Integer.Add;
|
||||
```
|
||||
- Otherwise, `T` is the type of `V`.
|
||||
```carbon
|
||||
let a: Integer = {};
|
||||
// `V` is `a`. `T` is the type of `V`, which is `Integer`.
|
||||
// `a.Add` refers to #2.
|
||||
let twice_a: Integer = a.Add(a);
|
||||
```
|
||||
|
||||
The appropriate `impl T as I` implementation is located. The program is invalid
|
||||
if no such `impl` exists. When `T` or `I` depends on a generic parameter, a
|
||||
suitable constraint must be specified to ensure that such an `impl` will exist.
|
||||
When `T` or `I` depends on a template parameter, this check is deferred until
|
||||
the argument for the template parameter is known.
|
||||
|
||||
`M` is replaced by the member of the `impl` that corresponds to `M`.
|
||||
|
||||
```carbon
|
||||
interface I {
|
||||
// #1
|
||||
default fn F[me: Self]() {}
|
||||
let N:! i32;
|
||||
}
|
||||
class C {
|
||||
impl as I where .N = 5 {
|
||||
// #2
|
||||
fn F[me: C]() {}
|
||||
}
|
||||
}
|
||||
|
||||
// `V` is `I` and `M` is `I.F`. Because `V` is a type-of-type,
|
||||
// `impl` lookup is not performed, and the alias binds to #1.
|
||||
alias A1 = I.F;
|
||||
|
||||
// `V` is `C` and `M` is `I.F`. Because `V` is a type, `impl`
|
||||
// lookup is performed with `T` being `C`, and the alias binds to #2.
|
||||
alias A2 = C.F;
|
||||
|
||||
let c: C = {};
|
||||
|
||||
// `V` is `c` and `M` is `I.N`. Because `V` is a non-type, `impl`
|
||||
// lookup is performed with `T` being the type of `c`, namely `C`, and
|
||||
// `M` becomes the associated constant from `impl C as I`.
|
||||
// The value of `Z` is 5.
|
||||
let Z: i32 = c.N;
|
||||
```
|
||||
|
||||
[Instance binding](#instance-binding) may also apply if the member is an
|
||||
instance member.
|
||||
|
||||
```carbon
|
||||
var c: C;
|
||||
// `V` is `c` and `M` is `I.F`. Because `V` is not a type, `T` is the
|
||||
// type of `c`, which is `C`. `impl` lookup is performed, and `M` is
|
||||
// replaced with #2. Then instance binding is performed.
|
||||
c.F();
|
||||
```
|
||||
|
||||
**Note:** When an interface member is added to a class by an alias, `impl`
|
||||
lookup is not performed as part of handling the alias, but will happen when
|
||||
naming the interface member as a member of the class.
|
||||
|
||||
```carbon
|
||||
interface Renderable {
|
||||
// #1
|
||||
fn Draw[me: Self]();
|
||||
}
|
||||
|
||||
class RoundWidget {
|
||||
external impl as Renderable {
|
||||
// #2
|
||||
fn Draw[me: Self]();
|
||||
}
|
||||
// `Draw` names the member of the `Renderable` interface.
|
||||
alias Draw = Renderable.Draw;
|
||||
}
|
||||
|
||||
class SquareWidget {
|
||||
// #3
|
||||
fn Draw[me: Self]() {}
|
||||
external impl as Renderable {
|
||||
alias Draw = Self.Draw;
|
||||
}
|
||||
}
|
||||
|
||||
fn DrawWidget(r: RoundWidget, s: SquareWidget) {
|
||||
// ✅ OK: In the inner member access, the name `Draw` resolves to the
|
||||
// member `Draw` of `Renderable`, #1, which `impl` lookup replaces with
|
||||
// the member `Draw` of `impl RoundWidget as Renderable`, #2.
|
||||
// The outer member access then forms a bound member function that
|
||||
// calls #2 on `r`, as described in "Instance binding".
|
||||
r.(RoundWidget.Draw)();
|
||||
|
||||
// ✅ OK: In the inner member access, the name `Draw` resolves to the
|
||||
// member `Draw` of `SquareWidget`, #3.
|
||||
// The outer member access then forms a bound member function that
|
||||
// calls #3 on `s`.
|
||||
s.(SquareWidget.Draw)();
|
||||
|
||||
// ❌ Error: In the inner member access, the name `Draw` resolves to the
|
||||
// member `Draw` of `SquareWidget`, #3.
|
||||
// The outer member access fails because we can't call
|
||||
// #3, `Draw[me: SquareWidget]()`, on a `RoundWidget` object `r`.
|
||||
r.(SquareWidget.Draw)();
|
||||
|
||||
// ❌ Error: In the inner member access, the name `Draw` resolves to the
|
||||
// member `Draw` of `Renderable`, #1, which `impl` lookup replaces with
|
||||
// the member `Draw` of `impl RoundWidget as Renderable`, #2.
|
||||
// The outer member access fails because we can't call
|
||||
// #2, `Draw[me: RoundWidget]()`, on a `SquareWidget` object `s`.
|
||||
s.(RoundWidget.Draw)();
|
||||
}
|
||||
|
||||
base class WidgetBase {
|
||||
// ✅ OK, even though `WidgetBase` does not implement `Renderable`.
|
||||
alias Draw = Renderable.Draw;
|
||||
fn DrawAll[T:! Renderable](v: Vector(T)) {
|
||||
for (var w: T in v) {
|
||||
// ✅ OK. Unqualified lookup for `Draw` finds alias `WidgetBase.Draw`
|
||||
// to `Renderable.Draw`, which does not perform `impl` lookup yet.
|
||||
// Then the indirect member access expression performs `impl` lookup
|
||||
// into `impl T as Renderable`, since `T` is known to implement
|
||||
// `Renderable`. Finally, the member function is bound to `w` as
|
||||
// described in "Instance binding".
|
||||
w.(Draw)();
|
||||
// ❌ Error: `Self.Draw` performs `impl` lookup, which fails
|
||||
// because `WidgetBase` does not implement `Renderable`.
|
||||
w.(Self.Draw)();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TriangleWidget extends WidgetBase {
|
||||
external impl as Renderable;
|
||||
}
|
||||
fn DrawTriangle(t: TriangleWidget) {
|
||||
// ✅ OK: name `Draw` resolves to `Draw` member of `WidgetBase`, which
|
||||
// is `Renderable.Draw`. Then impl lookup replaces that with `Draw`
|
||||
// member of `impl TriangleWidget as Renderable`.
|
||||
t.Draw();
|
||||
}
|
||||
```
|
||||
|
||||
## Instance binding
|
||||
|
||||
If member resolution and `impl` lookup produce a member `M` that is an instance
|
||||
member -- that is, a field or a method -- and the first operand `V` of `.` is a
|
||||
value other than a type, then _instance binding_ is performed, as follows:
|
||||
|
||||
- For a field member in class `C`, `V` is required to be of type `C` or of a
|
||||
type derived from `C`. The result is the corresponding subobject within `V`.
|
||||
The result is an lvalue if `V` is an lvalue.
|
||||
|
||||
```carbon
|
||||
var dims: auto = {.width = 1, .height = 2};
|
||||
// `dims.width` denotes the field `width` of the object `dims`.
|
||||
Print(dims.width);
|
||||
// `dims` is an lvalue, so `dims.height` is an lvalue.
|
||||
dims.height = 3;
|
||||
```
|
||||
|
||||
- For a method, the result is a _bound method_, which is a value `F` such that
|
||||
a function call `F(args)` behaves the same as a call to `M(args)` with the
|
||||
`me` parameter initialized by a corresponding recipient argument:
|
||||
|
||||
- If the method declares its `me` parameter with `addr`, the recipient
|
||||
argument is `&V`.
|
||||
- Otherwise, the recipient argument is `V`.
|
||||
|
||||
```carbon
|
||||
class Blob {
|
||||
fn Mutate[addr me: Self*](n: i32);
|
||||
}
|
||||
fn F(p: Blob*) {
|
||||
// ✅ OK, forms bound method `((*p).M)` and calls it.
|
||||
// This calls `Blob.Mutate` with `me` initialized by `&(*p)`
|
||||
// and `n` initialized by `5`.
|
||||
(*p).Mutate(5);
|
||||
|
||||
// ✅ OK, same as above.
|
||||
let bound_m: auto = (*p).Mutate;
|
||||
bound_m(5);
|
||||
}
|
||||
```
|
||||
|
||||
## Non-instance members
|
||||
|
||||
If instance binding is not performed, the result is the member `M` determined by
|
||||
member resolution and `impl` lookup. Evaluating the member access expression
|
||||
evaluates `V` and discards the result.
|
||||
|
||||
An expression that names an instance member, but for which instance binding is
|
||||
not performed, can only be used as the second operand of an indirect member
|
||||
access or as the target of an `alias` declaration.
|
||||
|
||||
```carbon
|
||||
class C {
|
||||
fn StaticMethod();
|
||||
var field: i32;
|
||||
class Nested {}
|
||||
}
|
||||
fn CallStaticMethod(c: C) {
|
||||
// ✅ OK, calls `C.StaticMethod`.
|
||||
C.StaticMethod();
|
||||
|
||||
// ✅ OK, evaluates expression `c` then calls `C.StaticMethod`.
|
||||
c.StaticMethod();
|
||||
|
||||
// ❌ Error: name of instance member `C.field` can only be used in a
|
||||
// member access or alias.
|
||||
C.field = 1;
|
||||
// ✅ OK, instance binding is performed by outer member access,
|
||||
// same as `c.field = 1;`
|
||||
c.(C.field) = 1;
|
||||
|
||||
// ✅ OK
|
||||
let T:! Type = C.Nested;
|
||||
// ❌ Error: value of `:!` binding is not constant because it
|
||||
// refers to local variable `c`.
|
||||
let U:! Type = c.Nested;
|
||||
}
|
||||
```
|
||||
|
||||
## Non-vacuous member access restriction
|
||||
|
||||
The first operand of a member access expression must be used in some way: a
|
||||
member access must either be direct, so the first operand is used for lookup, or
|
||||
must result in `impl` lookup, instance binding, or both.
|
||||
|
||||
```
|
||||
interface Printable {
|
||||
fn Print[me: Self]();
|
||||
}
|
||||
external impl i32 as Printable {
|
||||
fn Print[me: Self]();
|
||||
}
|
||||
fn MemberAccess(n: i32) {
|
||||
// ✅ OK: `Printable.Print` is the interface member.
|
||||
// `i32.(Printable.Print)` is the corresponding member of the `impl`.
|
||||
// `n.(i32.(Printable.Print))` is a bound member function naming that member.
|
||||
n.(i32.(Printable.Print))();
|
||||
|
||||
// ✅ Same as above, `n.(Printable.Print)` is effectively interpreted as
|
||||
// `n.(T.(Printable.Print))()`, where `T` is the type of `n`,
|
||||
// because `n` does not evaluate to a type. Performs impl lookup
|
||||
// and then instance binding.
|
||||
n.(Printable.Print)();
|
||||
}
|
||||
|
||||
// ✅ OK, member `Print` of interface `Printable`.
|
||||
alias X1 = Printable.Print;
|
||||
// ❌ Error, indirect access doesn't perform impl lookup or instance binding.
|
||||
alias X2 = Printable.(Printable.Print);
|
||||
// ✅ OK, member `Print` of `impl i32 as Printable`.
|
||||
alias X3 = i32.(Printable.Print);
|
||||
// ❌ Error, indirect access doesn't perform impl lookup or instance binding.
|
||||
alias X4 = i32.(i32.(Printable.Print));
|
||||
```
|
||||
|
||||
## Precedence and associativity
|
||||
|
||||
Member access expressions associate left-to-right:
|
||||
|
||||
```
|
||||
class A {
|
||||
class B {
|
||||
fn F();
|
||||
}
|
||||
}
|
||||
interface B {
|
||||
fn F();
|
||||
}
|
||||
external impl A as B;
|
||||
|
||||
fn Use(a: A) {
|
||||
// Calls member `F` of class `A.B`.
|
||||
(a.B).F();
|
||||
// Calls member `F` of interface `B`, as implemented by type `A`.
|
||||
a.(B.F)();
|
||||
// Same as `(a.B).F()`.
|
||||
a.B.F();
|
||||
}
|
||||
```
|
||||
|
||||
Member access has lower precedence than primary expressions, and higher
|
||||
precedence than all other expression forms.
|
||||
|
||||
```
|
||||
// ✅ OK, `*` has lower precedence than `.`. Same as `(A.B)*`.
|
||||
var p: A.B*;
|
||||
// ✅ OK, `1 + (X.Y)` not `(1 + X).Y`.
|
||||
var n: i32 = 1 + X.Y;
|
||||
```
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- [Separate syntax for static versus dynamic access, such as `::` versus `.`](/proposals/p0989.md#separate-syntax-for-static-versus-dynamic-access)
|
||||
- [Use a different lookup rule for names in templates](/proposals/p0989.md#use-a-different-lookup-rule-in-templates)
|
||||
- [Meaning of `Type.Interface](/proposals/p0989.md#meaning-of-typeinterface)
|
||||
|
||||
## References
|
||||
|
||||
- Proposal
|
||||
[#989: member access expressions](https://github.com/carbon-language/carbon-lang/pull/989)
|
||||
- [Question for leads: constrained template name lookup](https://github.com/carbon-language/carbon-lang/issues/949)
|
||||
@@ -0,0 +1,346 @@
|
||||
# Member access expressions
|
||||
|
||||
<!--
|
||||
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/989)
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Problem](#problem)
|
||||
- [Background](#background)
|
||||
- [Proposal](#proposal)
|
||||
- [Details](#details)
|
||||
- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
|
||||
- [Alternatives considered](#alternatives-considered)
|
||||
- [Separate syntax for static versus dynamic access](#separate-syntax-for-static-versus-dynamic-access)
|
||||
- [Use a different lookup rule in templates](#use-a-different-lookup-rule-in-templates)
|
||||
- [Meaning of `Type.Interface`](#meaning-of-typeinterface)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Problem
|
||||
|
||||
We need syntaxes for a number of closely-related operations:
|
||||
|
||||
- Given an expression denoting a package, namespace, class, interface, or
|
||||
similar, and the name of one of its members, form an expression denoting the
|
||||
member. In C++ and Rust, this is spelled `Container::MemberName`. In many
|
||||
other languages, it is spelled `Container.MemberName`.
|
||||
|
||||
- Given an expression denoting an object and a name of one of its fields, form
|
||||
an expression denoting the corresponding subobject. This is commonly written
|
||||
as `object.field`, with very little deviation across languages.
|
||||
|
||||
- Given an expression denoting an object and a name of one of its methods,
|
||||
form an expression that calls the function on the object. This is commonly
|
||||
written as `object.function(args)`.
|
||||
|
||||
- Given an expression denoting a type, and an expression denoting a member of
|
||||
an interface, form an expression denoting the corresponding member in the
|
||||
`impl` of that interface for that type.
|
||||
|
||||
Further, we need rules describing how the lookup for the member name is
|
||||
performed, and how this lookup behaves in generics and in templates in cases
|
||||
where the member name depends on the type or value of the first operand.
|
||||
|
||||
## Background
|
||||
|
||||
C++ and Rust distinguish between the first use case and the rest. Other
|
||||
languages, such as Swift and C#, do not, and model all of these use cases as
|
||||
some generalized form of member access, where the member might be a namespace
|
||||
member, an interface member, an instance member, or similar.
|
||||
|
||||
See also:
|
||||
|
||||
- [Exploration of member lookup in generic and non-generic contexts](https://docs.google.com/document/d/1-vw39x5YARpUZ0uD2xmKepLEKG7_u122CUJ67hNz3hk/edit)
|
||||
- [Question for leads: constrained template name lookup](https://github.com/carbon-language/carbon-lang/issues/949)
|
||||
|
||||
## Proposal
|
||||
|
||||
All these operations are performed using `.`:
|
||||
|
||||
```carbon
|
||||
fn F() {
|
||||
// Can perform lookup inside the package or namespace.
|
||||
var x: Package.Namespace.Class;
|
||||
// Can perform lookup inside the type of the value.
|
||||
x.some_field = x.SomeFunction(1, 2, 3);
|
||||
}
|
||||
```
|
||||
|
||||
When the type of the left-hand operand is a generic type parameter, lookup is
|
||||
performed in its type-of-type instead. Effectively, a generic type parameter
|
||||
behaves as an archetype:
|
||||
|
||||
```carbon
|
||||
interface Hashable {
|
||||
let HashValue:! Type;
|
||||
fn Hash[me: Self]() -> HashValue;
|
||||
fn HashInto[me: Self](s: HashState);
|
||||
}
|
||||
fn G[T:! Hashable](x: T) {
|
||||
// Can perform lookup inside the type-of-type if the type is
|
||||
// a generic type parameter.
|
||||
x.Hash();
|
||||
}
|
||||
```
|
||||
|
||||
When the type of the left-hand operand is a template parameter, the lookup is
|
||||
performed both in the actual type corresponding to that template parameter and
|
||||
in the archetype, as described above. If a result is found in only one lookup,
|
||||
or the same result is found in both lookups, that result is used. Otherwise, the
|
||||
member access is invalid.
|
||||
|
||||
```carbon
|
||||
class Potato {
|
||||
fn Mash[me: Self]();
|
||||
fn Hash[me: Self]();
|
||||
alias HashValue = Hashable.HashValue;
|
||||
}
|
||||
external impl Potato as Hashable where .HashValue = u32 {
|
||||
// ...
|
||||
}
|
||||
fn H[template T:! Hashable](x: T, s: HashState) {
|
||||
// When called with T == Potato:
|
||||
// ❌ Ambiguous, could be `Potato.Hash` or `Hashable.Hash`.
|
||||
x.Hash();
|
||||
// ✅ OK, found only in `Potato`.
|
||||
x.Mash();
|
||||
// ✅ OK, found only in `Hashable`.
|
||||
x.HashInto(s);
|
||||
|
||||
// ✅ OK, same `HashValue` found in both `Potato` and `Hashable`;
|
||||
// `Hashable.Hash` unambiguously names the interface member.
|
||||
var v: T.HashValue = x.(Hashable.Hash)();
|
||||
|
||||
// ✅ OK, unambiguously names the type member.
|
||||
x.(Potato.Hash)();
|
||||
}
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
See
|
||||
[the changes to the design](https://github.com/carbon-language/carbon-lang/pull/989/files).
|
||||
|
||||
## Rationale based on Carbon's goals
|
||||
|
||||
- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution)
|
||||
- Rejecting cases in a template where a generic interpretation and an
|
||||
interpretation with specific types would lead to different meanings
|
||||
supports incremental migration towards generics by way of a template,
|
||||
where the compiler will help you find places that would change meaning.
|
||||
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
|
||||
- Using a single, familiar `container.member` notation for all the member
|
||||
access use cases minimizes the complexity of this portion of the
|
||||
language syntax.
|
||||
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
|
||||
- The behavior of templates is aligned with that in C++, simplifying both
|
||||
comprehension for C++ developers and migration of C++ code.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Separate syntax for static versus dynamic access
|
||||
|
||||
We could follow C++ and Rust, and use `::` for static lookup, reserving `.` for
|
||||
instance binding:
|
||||
|
||||
```
|
||||
var x: Package::Namespace::Class;
|
||||
Class::Function();
|
||||
x.field = x.Function();
|
||||
x.(Interface::Method)();
|
||||
```
|
||||
|
||||
Advantages:
|
||||
|
||||
- Visually separates operations that readers may think of as being distinct: a
|
||||
`::` path statically identifies an object whereas a `.` path dynamically
|
||||
identifies a subobject or forms a bound method.
|
||||
- Improves familiarity for those coming from C++.
|
||||
- Removes most of the need for parenthesized member access: `a.(b.c)` would
|
||||
generally become `a.b::c`, like in C++.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Adds a new token and a new operation.
|
||||
- Swift, C#, and Java do not distinguish these operations syntactically, and
|
||||
we have no evidence that this lack of syntactic distinction creates problems
|
||||
for them in practice.
|
||||
- Likely to result in complexity and inconsistency for operations falling
|
||||
between the two options. For example, in C++:
|
||||
```
|
||||
struct A {
|
||||
static void F();
|
||||
enum { e };
|
||||
};
|
||||
enum class B { e };
|
||||
void G(A a, B b) {
|
||||
a.F(); // OK, but static dispatch, like A::F().
|
||||
a.e; // OK, but static dispatch, like A::e.
|
||||
b.e; // Error.
|
||||
}
|
||||
```
|
||||
- Does not provide an obvious syntax for `impl` lookup.
|
||||
`Type::Interface::method` would be ambiguous and `Type.Interface::method`
|
||||
would be inconsistent with using `::` for static lookup, so we would likely
|
||||
end up with `Type::(Interface::method)` syntax or similar.
|
||||
- May create the suggestion that `.`s imply a performance-relevant operation
|
||||
and `::`s do not. This will typically not be the case, as `.`s will
|
||||
typically result in, at worst, a constant offset. However, `impl` lookup,
|
||||
which may be performed by either a `.` or a `::`, may require a memory
|
||||
access in cases where dynamic dispatch is in use.
|
||||
|
||||
### Use a different lookup rule in templates
|
||||
|
||||
See
|
||||
[question for leads: constrained template name lookup](https://github.com/carbon-language/carbon-lang/issues/949)
|
||||
for more in-depth discussion and leads decision.
|
||||
|
||||
Given a situation where the same name can be found in both a type and a
|
||||
constraint when instantiating a template, and resolves to two different things,
|
||||
we could use various different rules to pick the outcome:
|
||||
|
||||
```
|
||||
class Potato {
|
||||
fn Bake[me: Self]();
|
||||
fn Hash[me: Self]();
|
||||
}
|
||||
interface Hashable {
|
||||
fn Hash[me: Self]() -> HashState;
|
||||
fn HashInto[me: Self](s: HashState);
|
||||
}
|
||||
external impl Potato as Hashable;
|
||||
|
||||
fn MakePotatoHash[template T:! Hashable](x: T, s: HashState) {
|
||||
x.Bake();
|
||||
x.Hash();
|
||||
x.HashInto(s);
|
||||
}
|
||||
```
|
||||
|
||||
We considered the following options:
|
||||
|
||||
| Option | Type only: `x.Bake()` | Both: `x.Hash()` | Constraint only: `x.HashInto(s)` |
|
||||
| --------------------- | --------------------- | ---------------- | -------------------------------- |
|
||||
| Type | -> Type | -> Type | ❌ Rejected |
|
||||
| Type over constraint | -> Type | -> Type | -> Constraint |
|
||||
| Type minus conflicts | -> Type | -> Type | ❌ Rejected |
|
||||
| Union minus conflicts | -> Type | ❌ Rejected | -> Constraint |
|
||||
| Constraint over type | -> Type | -> Constraint | -> Constraint |
|
||||
| Constraint | ❌ Rejected | -> Constraint | -> Constraint |
|
||||
|
||||
Of these rules:
|
||||
|
||||
- "Type" and "type over constraint" mean the constraints in a constrained
|
||||
template do not guide the meaning of the program, which creates a surprising
|
||||
discontinuity when migrating from templates to generics.
|
||||
- "Type minus conflicts" does not present a valuable improvement over "union
|
||||
minus conflicts".
|
||||
- "Union minus conflicts" makes the type-only case behave like a non-template,
|
||||
and the constraint-only case behave like a generic. This means that explicit
|
||||
qualification is necessary for all qualified names in a template if it wants
|
||||
to defend against ambiguity from newly-added names, whereas all the earlier
|
||||
options require qualification only for names intended to be found in the
|
||||
constraint, and all the later options require qualification for names
|
||||
intended to be found in the type. However, most of the other rules require
|
||||
explicit qualification in the same cases to defend against names being
|
||||
_removed_.
|
||||
- "Constraint over type" means there is potential for a discontinuity in
|
||||
behavior depending on whether we're able to symbolically resolve the type or
|
||||
not: if semantic analysis can determine a type symbolically, you get the
|
||||
behavior from the constraint, and if not, you get the behavior from the
|
||||
type. This may lead to surprising and hard-to-understand program behavior.
|
||||
- "Constraint" means that a constrained template behaves essentially the same
|
||||
as a generic, which harms the ability to use constrained templates as an
|
||||
incremental, evolutionary stepping stone from non-constrained templates into
|
||||
generics.
|
||||
|
||||
No rule provides ideal behavior. The most significant disadvantage of the chosen
|
||||
rule, "union minus conflicts", is that it requires explicit qualification with
|
||||
either the type or the constraint in a fully-robust template. However, the other
|
||||
leading contender, "constraint over type", also requires qualification of all
|
||||
names to prevent silent changes in behavior if a constraint is changed, and
|
||||
"union minus conflict" seems preferable to "constraint over type" in other ways.
|
||||
|
||||
### Meaning of `Type.Interface`
|
||||
|
||||
In this proposal, `impl` lookup is performed when a member of an interface
|
||||
appears on the right of a `.`. We could also consider applying `impl` lookup
|
||||
when the name of an interface appears on the right of a `.`. Under that
|
||||
alternative, `Class.(Interface)` would be a name for the `impl`, that is, for
|
||||
`impl Class as Interface`.
|
||||
|
||||
Because we have previously decided we don't want facet types, such a name would
|
||||
be restricted to only appear in the same places where package and namespace
|
||||
names can appear: on the left of a `.` or the right of an `alias`.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
interface MyInterface {
|
||||
fn F();
|
||||
fn G[me: Self]();
|
||||
}
|
||||
class MyClass {
|
||||
alias InterfaceAlias = MyInterface;
|
||||
impl as MyInterface {
|
||||
fn F();
|
||||
fn G[me: Self]();
|
||||
}
|
||||
}
|
||||
|
||||
fn G(x: MyClass) {
|
||||
// OK with this proposal and the alternative.
|
||||
MyClass.(MyInterface.F)();
|
||||
// Error with this proposal, OK with the alternative.
|
||||
MyClass.(MyInterface).F();
|
||||
|
||||
// Names the interface with this proposal.
|
||||
// Names the `impl` with the alternative.
|
||||
alias AnotherInterfaceAlias = MyClass.InterfaceAlias;
|
||||
|
||||
// Error with this proposal, OK with the alternative.
|
||||
MyClass.InterfaceAlias.F();
|
||||
// OK with this proposal, error with the alternative.
|
||||
MyClass.(MyClass.InterfaceAlias.F)();
|
||||
|
||||
// Error under this proposal, OK with the alternative.
|
||||
x.MyInterface.F();
|
||||
// Error under both this proposal.
|
||||
// Also error under the alternative, unless we introduce
|
||||
// a notion of a "bound `impl`" so that `x.MyInterface`
|
||||
// remembers its receiver object.
|
||||
x.MyInterface.G();
|
||||
// OK under this proposal and the alternative.
|
||||
x.(MyInterface.G)();
|
||||
}
|
||||
```
|
||||
|
||||
Advantages:
|
||||
|
||||
- Gives a way to name an `impl`.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- It's not clear that we need a way to name an `impl`.
|
||||
- Presents a barrier to supporting member interfaces, because
|
||||
`MyClass.MemberInterface` would name the `impl MemberInterface as MyClass`,
|
||||
not the interface itself.
|
||||
- Reintroduces facet types, without the ability to use them as a type. Having
|
||||
a way of naming an `impl` may lead to confusion over whether they are
|
||||
first-class entities.
|
||||
- Would either surprisingly reject constructs like `x.MyInterface.G()` or
|
||||
require additional complexity in the form of a "bound `impl`" value. The
|
||||
value of such a type would presumably be equivalent to a facet type.
|
||||
|
||||
As a variant of this alternative, we could disallow `Type.Interface` for now, in
|
||||
order to reserve syntactic space for a future decision. However, it's not clear
|
||||
that the cost of evolution nor the likelihood of such a change is sufficiently
|
||||
high to warrant including such a rule.
|
||||
Reference in New Issue
Block a user