The four different sorts of type definition are each intended for different purposes. The most commonly used is the class definition.
A qualified type name is resolved by first searching for the first part of the name in the following places, in order:
Once a package or a type definition for the first part of the name is found, the rest of the names are resolved recursively underneath it.
Classes are used to specify the state and behaviour of a type of object.
A class can extend a single other class (as long as that class does not extend it), and can implement multiple interfaces. It can be defined as follows:
<modifier>* class <name> [extends <qualified-name>] [implements <qualified-name>[, <qualified-name>]*]
{
<member>*
}
Or, as an LALR(1) grammar:
ClassTypeDef = OptionalModifiers class Name ImplementsClause { MemberList } |
OptionalModifiers class Name extends QName ImplementsClause { MemberList }
ImplementsClause = implements InterfaceList | ε
InterfaceList = QName | InterfaceList , QName
Possible modifiers for classes are:
abstractMakes it impossible to create an instance of the class directly, only its subclasses may be created.immutableMakes all references to this type immutable, and implicitly makes all methods of this type immutable.since(1.2.3)Has no effect, but can be useful for documentation. Any version number can be used inside the brackets.
The qualified name in the extends clause must refer to another class definition, and the ones in the implements clause must refer to interface definitions.
If a class is not marked as abstract, it may not declare abstract methods or properties.
If a class extends or implements another immutable type, it must be immutable itself.
Interfaces are used to specify some behaviour that objects can implement.
An interface can extend multiple other interfaces. It can be defined as follows:
<modifier>* interface <name> [extends <qualified-name>[, <qualified-name>]*]
{
<member>*
}
Or, as an LALR(1) grammar:
InterfaceTypeDef = OptionalModifiers interface Name { MemberList } |
OptionalModifiers interface extends InterfaceList Name { MemberList }
Possible modifiers for interfaces are:
immutableMakes all references to this type immutable, and implicitly makes all methods of this type immutable.since(1.2.3)Has no effect, but can be useful for documentation. Any version number can be used inside the brackets.
The qualified names inside the extends clause must refer to interface definitions.
Interfaces cannot have fields or properties with backing variables, unless they are static. Any methods declared on an interface are abstract by default, unless they provide is an implementation (or a native name).
If an interface extends an immutable interface, it must be immutable itself.
TODO
Classes, interfaces, and enums can all extend certain other types. In order for the language to work, we have to place some restrictions on the layout of the inheritance hierarchy.
Most obviously, a type cannot inherit from itself, so all forms of circular inheritance are forbidden.
To resolve the members of a type, the inheritance hierarchy must be linearised into a predictable order. When searching for a member of a type, the type definitions are searched in the order of this linearisation. Plinth uses a technique known as C3 Linearisation to give a predictable order to the inheritance hierarchy.
Each type definition has a list of super-types. The order of this list is the order they are declared in. In a class, the first element in the list is the super-class (if any), followed by the interfaces.
To find the C3 Linearisation of a type the C3 Linearisations of each of the super-types must be known in advance, and are also used as input lists to the linearisation algorithm.
First, a list of these lists must be created. This consists of each of the super-types’ linearisation lists, followed by the super-type list for the current type. This super-type list contains the current type itself, followed by the list of its super-types in the order they are declared in.
Given this list of lists, we can either calculate the linearisation, or determine that none exists. In the case that none exists, a compiler error is emitted.
We now search (in order) through the list of lists for one whose first element only occurs at the start of any of the lists. The first such type we find is the next element we pick, and it is appended to the linearisation and removed from each of our lists. This process is repeated until no more elements can be picked. If all of the lists are empty, we have a valid linearisation; if they are not, it is a badly defined type hierarchy, which is forbidden.
Value types are similar to class types, but instead of being stored on the heap, they are stored on the stack and passed by value. They cannot inherit from any other types or implement interfaces, and they cannot be inherited from by anything.
Value types can be defined as follows:
<modifier>* compound <name>
{
<member>*
}
Or, as an LALR(1) grammar:
CompoundTypeDef = OptionalModifiers compound Name { MemberList }
Possible modifiers for value types are:
immutableMakes all references to this type immutable, and implicitly makes all methods of this type immutable.since(1.2.3)Has no effect, but can be useful for documentation. Any version number can be used inside the brackets.
Value types are implemented very similarly to struct types in C. One similarity is that a value type cannot contain another of itself as part of one of its own fields. For example, the following types create a field-loop and are therefore invalid:
compound foo {
bar a;
}
compound bar {
foo b;
ubyte c;
}
This is because in order to store a foo, you must store a bar directly inside it, but there is a foo directly inside that, meaning that an allocation of either of these values would require an infinite amount of memory.