LEVIATHAN v962456e · 962456eee1

Expressions

Member access and qualification

The dot for instances, the double colon for base, static and namespace names, optional chaining, and the explicit-type marker.

since 0.1.0-alpha.1linuxwindowswasm

Description

Two operators navigate to a member, and they mean different things:

  • . navigates an instance: obj.field, obj.method(args).
  • :: navigates the non-instantiated side: a base class (this.Counter::value), a constructor label (Type::Label), a namespace (NS::name), or the static side of a class. Inside a generic function, the left operand may also be a type parameter of that function, resolved separately for each concrete instantiation.

There is one rule behind both: :: means "the non-instantiated version".

?. is optional chaining. a?.m() and a?.b evaluate to None when a is None, without evaluating the call's arguments, and otherwise behave like .. The result type is the member's type or None.

::<...> is the explicit-type marker. It attaches to the complete callee, as in N::f::<T>() and this.Base::m::<T>(). Followed by an argument list it fixes the type arguments of a call or construction. Without an argument list it makes a reference to a generic function, as in var f = identity::<int>;.

Qualifying a base, a namespace and an optional receiver

class Counter {
    distinct int value = 1;
}

class Gauge {
    distinct int value = 2;
}

class Both : Counter, Gauge {
    int sum() => this.Counter::value + this.Gauge::value;
}

class Animal {
    string speak() => "...";
}

class Dog : Animal {
    string speak() => "Woof";
    string both() => "${this.speak()} / ${this.Animal::speak()}";
}

class Node {
    string name = "node";
}

namespace Util {
    int twice(int n) => n * 2;
}

T identity<T>(T x) => x;

void main() {
    console.writeln(Both().sum());
    console.writeln(Dog().both());
    console.writeln(Util::twice(4));

    Node? missing = None;
    Node? present = Node();
    console.writeln(missing?.name ?? "none");
    console.writeln(present?.name ?? "none");

    var pinned = identity::<int>;
    console.writeln(pinned(3));
}
main();
3
Woof / ...
8
none
node
3

Rules

  • . reaches instance members; :: reaches base-class, static, label and namespace members.
  • Qualifying with a base class (this.Animal::speak()) calls that class's own member where the bare name is unambiguous, bypassing any override in the derived class. Where the bare name is ambiguous between distinct paths, it selects the path and dispatches on the runtime class (lang.distinct).
  • Two unrelated declarations of one name and type in two bases are a compile error unless the class restates the member. A field declared distinct in either base keeps its own slot, reachable only by qualification (this.Counter::value). A bare read of such a field (b.value) is a compile error: the compiler refuses to guess which slot is meant. A qualifier may be any class on the path.
  • Optional chaining ?. skips the call and its arguments when the receiver is None.
  • A ::-reached callable that is not followed by ( is a method reference. See lang.method-references.
  • A base qualifier x.Base::member names one of the bases of x's class. Base is decided in this order. If the name, looked up where the access is written, is a base of x's class, it is that base, even when x's class also has a member named Base. Otherwise, if x's class has a field, method or accessor named Base, x.Base reads that member: h.Node::v on a field named Node reads the field. Otherwise Base is matched by name against the bases of x's class, so c.Counter::value also works outside the namespace that declares Counter. When the static type of x is not a class (a type parameter or an interface), x.Base is a qualifier only when Base names a top-level class. When the match by name finds two different bases, the qualifier is ambiguous and the access is a compile error:
error: ambiguous base qualifier 'A': 'C' has more than one base class named 'A'
  • The qualifier is looked up where the access is written first, so inside namespace N the qualifier A names N::A even when another base is M::A; alias the other (use M::A as MA;) to name it. The qualified class must declare or inherit the member:
error: type 'N::A' has no member 'w'

Examples

Optional chaining does not evaluate the arguments of a skipped call

class Probe {
    int add(int k) => k;
}

int count = 0;

int tick() {
    count = count + 1;
    return count;
}

Probe? none = None;
Probe? some = Probe();
console.writeln(none?.add(tick()) ?? -1);
console.writeln(count);
console.writeln(some?.add(tick()) ?? -1);
console.writeln(count);
-1
0
1
1
class Counter { distinct int value = 1; }
class Gauge { distinct int value = 2; }
class Both : Counter, Gauge { }

Both b = Both();
console.writeln(b.value);
// error: ambiguous read of 'value : int' (distinct on multiple bases); qualify with '::'
not run — shows a compile error

Notes

Member access or a call on a value of a union type, including an optional T?, is a compile error until the value has been narrowed to one member. Narrow with if (x != None) { ... } or use ?..

See also

  • Map — An associative collection from keys of type K to values of type V, with value semantics.
  • Method references — Naming a function, method or labeled constructor without calling it yields an ordinary function value.