Class semantics in Water

Classes behave in a similar way to how they do in many other object-oriented languages. They can store multiple pieces of data, and can have functions and custom operators that manipulate their data.

Basic class use

Fields and methods

Classes are composed of many fields. While this is similar to multivalued types, classes allow you to access different fields by their name instead of their indices.

Classes also serve as a namespace for storing functions related to working with the data they describe. Unlike many other languages, Water does not make a distinction between instance methods and static methods: all methods can be considered static. However, if the first parameter of a method is the class's type, then it can be called as if it were an object's instance method.

Every class has a constructor method. This method is called when the class is first initialised. This is used for setting up its properties and any other state, as necessary.

In Water, a constructor's first parameter must be a reference to the class instance. When instantiating the class, this parameter is passed automatically, as it is generated by the allocator which you used to allocate the class. A constructor also returns the class instance, usually the same one it got passed as a parameter. A class constructor is defined by adding the [Constructor] attribute to a method in the class. You can do this to multiple methods, in which case, they serve as different constructor overloads.

Class inheritance

Inheritance works much the same as in any other object-oriented language. A class extending another class will have access to all fields and methods from the extending class, including the ones it defines itself.

Extended classes are instances of both themselves and any of the parent types they extend. This property is particularly useful for cases like creating polymorphic arrays, where many class types can be stored in a single data structure based on some shared class they all inherit from.

Warning
Method overriding (i.e. virtual functions) is currently not supported. As an alternative, you may store function references directly as properties of the class, or have a reference to a struct of function references which could both function as vtables.

Warning
Note that currently, multiple inheritance is not supported. This is because the current iteration of the Wasm GC standard does not support extending multiple structs, which would complicate the implementation of related functionality like instanceof checks. Multiple inheritance is also not yet implemented for classes allocated on the stack or using a custom allocator, but is likely to be available sooner.

Template / generic classes

In Water, you are able to create template classes, which can be used to create separate compile-time variations of the classes. This is a really powerful technique for reusing code.

Class templates in Water are more powerful than generics from other languages, and are called templates because they are more akin to C++'s templates. Like in C++, any compile-time constant value can be used as a template instantiation parameter, and not just types.

There is no way to set constraints on which types or values may be used to instantiate the template. Again, this is the same as in C++, where if the provided value or type is used in a way that it supports (e.g. the code doesn't use any operators or methods which aren't defined on the template type), then the code will compile.

The instantiated class type inside of classes is aliased with `Class`, because with templates, always writing out the full type might become messy. To be clear, in the following example, `Class` is the same as `Vector<Type, Elements>`.

The below example creates a template vector class with a few example methods, which can use an arbitrary number type for its elements, and can have an arbitrary amount of elements:

Operator overloading

Water makes it possible to overload operators on the classes you create. You can turn any class method into an overloaded operator by giving it an attribute like [Operator("+")]. All operators except for the "." property access operator can be overloaded.

This next example extends the code from the previous snippet to use operator overloading. See lines 14, 24 and 33 for the [Operator(...)] attribute being used, and lines 64 and 75 where the overloaded operators are used.

Instantiating and storing classes

Class instances can be stored in three different ways:

1. Storing classes on the stack

This is useful for when the data in the class is being updated frequently, as memory access patterns can heavily be optimised when stored in this form. This is equivalent to how primitive types like numbers and strings are stored in local variables.

However, this is not suitable for longer-term storage, and is not very flexible as it is impossible to create references to stack-allocated objects. Stack-allocated classes are passed by-value, which means that the instance's value is copied when passed to and from functions. This effectively translates to local variables in WebAssembly.

2. Storing classes using GC objects

This stores class instances as WebAssembly GC objects. This form is very easy to use and flexible, especially because objects are cleaned up automatically when they are no longer needed. If you are unsure about the other options, this is likely the best method as very little can go wrong here. GC-allocated class objects are passed by-reference.

References to GC-allocated objects can be stored in tables. This is the best way of persisting references to these objects across multiple function calls.

You can also store GC-allocated objects in global variables. The global variable could also be an array, allowing you to store multiple objects at the same time.

There are a few differences between using tables and global arrays to store multiple object references. In short, tables are resizable but can not be reassigned or moved, while global arrays are statically sized, but can be reassigned. Tables are also much easier to access from the embedder, e.g. from Javascript. You will likely get better code generation if tables are used for storing the root of persistent state, but as always, use profiling tools to determine the best method for your use-case.

3. Storing classes using custom allocators

This stores class instances using some custom user-defined allocation algorithm, usually resulting in the objects being stored in linear memory. This is the most portable method for longer-term object storage, as it does not necessarily rely on more modern WebAssembly features. It also provides the most low-level optimisation opportunities: due to being stored in linear memory, it is possible to work directly with things like atomics, SIMD, and bulk memory operations.