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:
| Type | Description |
|---|---|
| i32, u32 | A 32-bit integer type. i32 is signed (can be positive or negative), and u32 is unsigned (always positive). |
| i64, u64 | A 64-bit integer type. i64 is signed, and u64 is unsigned. When passed to Javascript, it is converted to a BigInt. |
| f32, f64 | Floating point (decimal) numbers. f32 is less precise than f64, but takes up half as much space in memory. |
| string | Technically an alias of externref; is used for strong-typing strings for ease of use. |
| v128 | Packed vector type for SIMD. Find out more at the SIMD proposal. |
| i8x16, u8x16, i16x8, u16x8, i32x4, u32x4, i64x2, u64x2, f32x4, f64x2 | Effectively aliases for the v128 SIMD type, which offer optional strongly typed SIMD semantics for ease of use. |
| i8, u8, i16, u16 | Packed storage types. A value can not have these types, but they can be used to reduce the amount of storage needed for a number. |
| all | This is equivalent to Typescript's `any` type, and means that all types are possible. |
| never | This 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. |
| any | This 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. |
| none | This is the bottom type of all heap-allocatable types. Find out more here. |
| i31, u31 | Unboxed scalar heap values, stored instead of a pointer. Find out more here. |
| externref | A reference to an external object (e.g. non-primitive value from Javascript). |
| exnref | A reference to an exception. Note that this is NOT the same as externref! |
| func | A general function type. |
| noexternref, noexnref, nofunc | The 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.