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:
Richard Smith
2022-03-02 13:01:01 -08:00
committed by GitHub
co-authored by josh11b
parent 7cce1bd124
commit 18b423dc5f
3 changed files with 1084 additions and 1 deletions
+85 -1
View File
@@ -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:
+653
View File
@@ -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)
+346
View File
@@ -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.