LEVIATHAN v962456e · 962456eee1

Declarations

distinct fields

Keep same-named fields of different bases in separate slots, reached by qualification.

since 0.1.0-alpha.1linuxwindowswasm

Description

When two bases of a class declare a field with the same name and type, the class must say what it wants: restating the field gives one slot, and two unrelated declarations are otherwise a compile error (see lang.multiple-inheritance). Marking a field distinct keeps both declarations apart:

public distinct int value = 0;

A distinct member keeps its own slot, apart from every same-named member of another base. The slot belongs to the declaration: a member declared once is one slot, however many inheritance paths bring it into a class. A subclass on that path that redeclares the member overrides the slot. It changes what the slot holds (a field's initial value, a method's body) and adds no slot, so the slot always holds the most-derived declaration on its path. Each slot is reached by naming a class on its path: this.Counter::value, or c.Counter::value from outside.

Two distinct slots with one name

class Counter { public distinct int value = 10; }
class Timer { public distinct int value = 20; }
class Clock : Counter, Timer {
    int total() => this.Counter::value + this.Timer::value;
}
Clock c = Clock();
c.Counter::value = 11;
console.writeln(c.Counter::value);
console.writeln(c.Timer::value);
console.writeln(c.total());
11
20
31

Rules

  • A collision is two fields of the same name and the same type inherited from different bases. Without distinct, two unrelated declarations of one name and type are a compile error; see lang.multiple-inheritance.
  • If distinct appears on either side of a collision, the two declarations keep separate slots. A distinct member declared once and inherited along several paths is still one slot.
  • A separate slot is reached through a qualified path object.Base::name, where Base is the class that declares the member or any class that inherits it along that path. A qualified field reads or writes that path's slot. A qualified method call selects the path where the bare call would be ambiguous between distinct paths, and then runs the most-derived body on that path for the receiver's runtime class. Where the bare call is unambiguous, it is a static call of Base's own body, the way an override calls the method it overrides. Where only one slot of that name is visible — inside the declaring class and its descendants on a single chain, or through a receiver typed as one of them — the bare name reaches it.
  • A subclass that redeclares an inherited distinct member overrides its slot, whether or not it repeats distinct. The redeclaration must agree with the member on const, readonly and weak. Where the class also inherits a plain member of the same name and type, a distinct redeclaration overrides the distinct member and leaves the plain one as it is. A plain redeclaration there would match both members, and a class that inherits the member on two paths has no one path to override, so in both cases the redeclaration is an error:
error: 'hp : int' in 'Leaf' redeclares a `distinct` member that reaches it on two paths, 'Race::hp' and 'Job::hp'; override it in a class on one path instead
  • A member written distinct over an inherited member that is not distinct is not an override: it asks for a second slot beside the inherited one.
  • Two unrelated classes that each override the same distinct member compete for its one slot. A class that inherits both must redeclare the member to choose:
error: 'hp : int' reaches 'X' through two competing overrides of the `distinct` member 'Race::hp', 'Elf::hp' and 'Dwarf::hp'; declare 'hp' in 'X' to choose
  • A bare read of a name that has more than one distinct slot is a compile error that asks you to qualify the name with ::.
  • A bare call of a distinct method that is visible on two paths is the same error: the call names no path, so the compiler asks for a qualifier instead of choosing one.
error: ambiguous call of 'hpMod() -> int' (distinct on multiple bases); qualify with '::'
  • An accessor is bound to the one field of its name in the class that declares it. A class that holds two fields of that name cannot declare an accessor for it:
error: accessor 'value' in 'Widget' is ambiguous: 'value' reaches 'Widget' as 'Counter::value' and 'value' (distinct on multiple bases); declare the accessor in a class on one path

Examples

distinct on one side only

class A { distinct int v = 1; }
class B { int v = 2; }
class C : A, B { }
C c = C();
console.writeln(c.A::v);
console.writeln(c.B::v);
1
2

An unqualified read is rejected:

An ambiguous bare read

class Counter { public distinct int value = 10; }
class Timer { public distinct int value = 20; }
class Clock : Counter, Timer { }
Clock c = Clock();
console.writeln(c.value);
// error: ambiguous read of 'value : int' (distinct on multiple bases); qualify with '::'
not run — a compile error on purpose

A subclass that redeclares an inherited distinct field on a single chain overrides it, so the chain behaves as single inheritance:

A single chain behaves as single inheritance

class Race { distinct int hp = 0; }
class Elf : Race { int hp = 2; }
console.writeln(Elf().hp);
2

Each axis of a combined class can redeclare its distinct members. The combined class reaches each path by qualification: a field reads that path's slot, and a method call runs that path's override:

Redeclaring a distinct member overrides its path

class Race { distinct string name = "?"; distinct int hp = 0; distinct string cry() => "..."; }
class Job  { distinct string name = "?"; distinct int hp = 0; distinct string cry() => "..."; }

class Elf     : Race { string name = "Elf";     int hp = 2; string cry() => "For the grove!"; }
class Fighter : Job  { string name = "Fighter"; int hp = 5; string cry() => "Hold the line!"; }

class Unit : Race, Job {
    int maxHp()    => 10 + this.Race::hp + this.Job::hp;
    string title() => this.Race::name + " " + this.Job::name;
    string shout() => this.Race::cry() + " " + this.Job::cry();
}
class Archer : Unit, Elf, Fighter { }

Archer a = Archer();
console.writeln(a.title());
console.writeln(a.maxHp());
console.writeln(a.shout());
Elf Fighter
17
For the grove! Hold the line!

Two classes with the same name from different namespaces can both contribute distinct members. Inside a namespace the bare qualifier names that namespace's class, and an alias names the other:

Same-named bases from two namespaces

namespace M { class A { distinct int v = 2; string who() => "M::A"; } }
namespace N {
  use M::A as MA;
  class A { distinct int v = 1; string who() => "N::A"; }
  class C : A, MA {
    string who() => "C";
    int f() => this.A::v;
    int g() => this.MA::v;
    string w() => this.A::who() + " " + this.MA::who();
    void set() { this.A::v = 10; this.MA::v = 20; }
  }
}
N::C c = N::C();
console.writeln(c.f().toString() + " " + c.g().toString() + " " + c.w());
c.set();
console.writeln(c.f().toString() + " " + c.g().toString());
1 2 N::A M::A
10 20

Notes

Limits:

  • A member declared distinct once and inherited along several paths is one slot. Qualifying with different classes on those paths (this.Race::hp, this.Job::hp when both derive from the declaring class) names the same slot.
  • Storage keys and dispatch names use the namespace-qualified class name (N::A::v), so two classes with the same name from different namespaces may both contribute distinct members to one class. Inside a namespace the bare qualifier names that namespace's class; the other is named through an alias (use M::A as MA; then this.MA::v).

See also

  • Multiple inheritance — Combine several base classes, how same-named fields and methods collapse or stay separate, and how to reach one base's member by qualification.
  • Fields — Declare the data a class or struct holds, with optional initializers, and how a field without an initializer gets its default value.