diff --git a/docs/design/expressions/README.md b/docs/design/expressions/README.md index dae12350ca2a6..7ce3f78bb3825 100644 --- a/docs/design/expressions/README.md +++ b/docs/design/expressions/README.md @@ -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
+ 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: diff --git a/docs/design/expressions/member_access.md b/docs/design/expressions/member_access.md new file mode 100644 index 0000000000000..1d3d18b288b11 --- /dev/null +++ b/docs/design/expressions/member_access.md @@ -0,0 +1,653 @@ +# Qualified names and member access + + + + + +## 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) + + + +## 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) diff --git a/proposals/p0989.md b/proposals/p0989.md new file mode 100644 index 0000000000000..4c5cf0bcd9005 --- /dev/null +++ b/proposals/p0989.md @@ -0,0 +1,346 @@ +# Member access expressions + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/989) + + + +## 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) + + + +## 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.