Skip to content

Latest commit

 

History

History
656 lines (540 loc) · 20.8 KB

member_access.md

File metadata and controls

656 lines (540 loc) · 20.8 KB

Qualified names and member access

Table of contents

Overview

A qualified name is a word 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 simple member access expression of the form:

  • member-access-expression ::= expression . word

or a compound member access of the form:

  • member-access-expression ::= expression . ( expression )

Compound member accesses allow specifying a qualified member name.

For example:

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 to a specific member entity, called M in this document.
  • Then, if necessary, 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 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, 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 expression must be a simple member access expression. 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 compound 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.

For a simple 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.

For a compound 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.

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.

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 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.

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.
    // `V` is `Integer`. `T` is `V`, which is `Integer`.
    // Alias refers to #2.
    alias AddIntegers = Integer.Add;
  • Otherwise, T is the type of V.
    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.

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 may also apply if the member is an instance member.

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.

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 compound 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.

    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.
    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 a compound member access or as the target of an alias declaration.

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 compound member access must result in impl lookup, instance binding, or both. In a simple member access, this always holds, because the first operand is always used for lookup.

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, compound 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, compound 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

References