Type system

Water's type system can go quite deep, but on the surface, it's quite simple.

Primitive types

To start off simple, Water has a handful of primitive types. These are roughly the same primitive types that core WebAssembly has. Here is an overview of all of them:

TypeDescription
i32, u32A 32-bit integer type. i32 is signed (can be positive or negative), and u32 is unsigned (always positive).
i64, u64A 64-bit integer type. i64 is signed, and u64 is unsigned. When passed to Javascript, it is converted to a BigInt.
f32, f64Floating point (decimal) numbers. f32 is less precise than f64, but takes up half as much space in memory.
stringTechnically an alias of externref; is used for strong-typing strings for ease of use.
v128Packed vector type for SIMD. Find out more at the SIMD proposal.
i8x16, u8x16, i16x8, u16x8, i32x4, u32x4, i64x2, u64x2, f32x4, f64x2Effectively aliases for the v128 SIMD type, which offer optional strongly typed SIMD semantics for ease of use.
i8, u8, i16, u16Packed storage types. A value can not have these types, but they can be used to reduce the amount of storage needed for a number.
allThis is equivalent to Typescript's `any` type, and means that all types are possible.
neverThis is equivalent to Typescript's `never` type, and is found in expressions which never yield a value, e.g. the type of break statements, return statements, unreachable, etc.
anyThis type is the top type of all heap-allocatable types. It is used when the type of some object is not yet known, or does not need to be known. Find out more here.
noneThis is the bottom type of all heap-allocatable types. Find out more here.
i31, u31Unboxed scalar heap values, stored instead of a pointer. Find out more here.
externrefA reference to an external object (e.g. non-primitive value from Javascript).
exnrefA reference to an exception. Note that this is NOT the same as externref!
funcA general function type.
noexternref, noexnref, nofuncThe bottom types of externref, exnref, and func.

Multivalued types

Water has first-class support for multivalued types. They are similar to tuples or aggregations in other languages, except that they can't be (directly) nested: they will always be in a flat hierarchy.

This is useful for instances where a function naturally returns more than a single value, and creating an entire class to store the components would be too verbose. `unroll` statements which yield values also produce multivalued types, where each component contains the result of its corresponding iteration.

The fact that multivalued types always retain a flat hierarchy can also be useful for aggregating multiple individual function parameters into a single one. The example below shows that: calling `i32.add` with two `i32` parameters is identical to passing a single `i32, i32` parameter.

Here are some examples of multivalued types being used:

Note that multivalued types are indexed with the angled brackets, and not the square brackets. This is because there would otherwise be a syntax conflict with array type declarations.

Multivalued types can contain differing component types. For this reason, multivalued types can only be indexed with const indices. You can also shuffle or select certain values from multivalued types by providing multiple indices in the angled brackets.

Multivalued types allow for very interesting compile-time recursion. As they can intrinsically represent more than one parameter, generic template functions can be written that e.g. sum all of the components of a multivalued type recursively. The `#` prefix operator reports the number of components in a type, and the `..` range operator creates a multivalued type containing the integer indices between the two dots, inclusive.

Classes

You can find a comprehensive overview of classes here. Note that this also covers GC pointer types, and custom allocator types.

Function types

Technically, Water has three different types which can represent functions: `(a -> b)`, `(a -> b)*` and `(a => b)*`.

The first type is the type of any ordinary function defined in Water source code. It is almost never used in code, because new functions can not be created nor reassigned due to WebAssembly's typing and function linking rules.

The other two types, `(a -> b)*` and `(a => b)*`, are distinguished by the fact that the first is a function reference type, and the second is a closure type. Function references are written with a minus "-", whereas closures are written with an equals sign "=".

From a high level, the only difference between these two is that the function reference can not capture its surrounding scope, while the closure can. Function references can also somewhat easily be called from the host Javascript environment, while closures are a bit more involved.

Apart from the arrow, function references and closures have the exact same syntax. The syntax is almost the same as closures in Javascript, except that parameter types must always be specified.