Vectors

Vectors are vectorized variants of primitive types to increase both readability, performance (SIMD) and ease of use for vectorized math operations and much more. Here is an example of how vectors work:

use Core.print

def main():
    i32x3 v3 = (1, 2, 3);
    print($"v3.(x, y, z) = ({v3.x}, {v3.y}, {v3.z})\n");
    v3.(x, y, z) = (4, 5, 6);
    print($"v3 = {v3}\n");

This program will print these lines to the console:

v3.(x, y, z) = (1, 2, 3)
v3 = (4, 5, 6)

As you can see, the 3-width i32 vector has the "fields" x, y and z, each being of type i32. There exist several vector types in Flint today, to be more precise, every single integer and floating-point literal has multiple vector variations of it:

TypeElement TypeVector Size
u8x2u82
u8x3u83
u8x4u84
u8x8u88

i8x2i82
i8x3i83
i8x4i84
i8x8i88

u16x2u162
u16x3u163
u16x4u164
u16x8u168

i16x2i162
i16x3i163
i16x4i164
i16x8i168

u32x2u322
u32x3u323
u32x4u324
u32x8u328

i32x2i322
i32x3i323
i32x4i324
i32x8i328

u64x2u642
u64x3u643
u64x4u644

i64x2i642
i64x3i643
i64x4i644

f32x2f322
f32x3f323
f32x4f324
f32x8f328

f64x2f642
f64x3f643
f64x4f644

bool8bool8

All vectors with up to width 4 can be accessed via the field names directly, while all vectors larger than 4, like i32x8, can only be accessed with the same index-based accesser like tuples through the .$N syntax. This is also the reason why tuples needed to be explained before vectors. There exist several aliases for each component, each being unambiguous. Below is a table describing which "field" names exist for each component:

WidthField 0Field 1Field 2Field 3
2$0$1
3$0$1$2
4$0$1$2$3

2uv

2ij
3ijk
4ijkl

2xy
3xyz
4xyzw

2st
3stp
4stpq

3rgb
4rgba

As you can see, different widths have different field names for the coordinates. The u and v fields for vectors of size two, for example, are used a lot in UV-coordinate systems, and rgba just starts at three components, because just having r and g for vectors of width 2 does not make any sense.

The names are designed in a way that eliminates collisions in every case. The same letter will always be used for the same coordinate. If we would have added uvw, like it nomally would be, the w would collide with the fourth field in xyzw, so now it would be ambiguous whether w is the third or fourth field of a vector without knowing the type upfront, which would be really bad UX. So, instead we opted to design the names in a way that completely eliminates ambiguity.

If you really want to disambiguate the index of the vector, for example when a is the index 0 in other languages or libraries (argb format exists), then you can still use the .$0 coordinate and this unambiguously always just means "first element of vector".

Vectors with Functions

Lets move on to functions, because vectors can be returned from functions too, unlike tuples. So, we can very well define a function like this:

use Core.print

def get_vec_2(i32 x, i32 y) -> i32x2:
    return (x, y);

def main():
    (x, y) := get_vec_2(10, 20);
    print($"(x, y) = ({x}, {y})\n");

This program will print this line to the console:

(x, y) = (10, 20)

As you can see, interoperability between vectors and groups just works. Groups are Flint's "type interoperability layer". You can pack multiple single values into a group, then store it in a tuple. Or access multiple fields of a tuple and store it in a vector etc. Groups are the real "middle-ground" of Flint's type system, because you can return a group of (i32, i32) and still store it in a vector or you can return a i32x2 and store it in a group. The group, however, could also be a grouped assignment of a tuple, so you could very well write tuple.($0, $2) = get_vec_2(10, 20); and store the i32x2 return value on the $0 and $2 fields of the tuple, because its a grouped assignment and groups are natively meant to be interoperable with Flint's other types.

Vector Arithmetic

Vectors are primitive types in Flint, which means that they have first-class arithmetic support. The vector variant of any type supports the same arithmetic operations as its underlying type. Here is one example of this:

use Core.print

def main():
    i32x4 v4_1 = (1, 2, 3, 4);
    i32x4 v4_2 = (5, 6, 7, 8);
    i32x4 sum = v4_1 + v4_2;
    print($"sum = {sum}\n");

This program will print this line to the console:

sum = (6, 8, 10, 12)

Constructors

Vectors can also be constructed using the constructor syntax T{}:

use Core.print

def main():
	v1 := i32x4{};                 // Default-construction
	v2 := i32x4{.x = 3, .z = 5};   // Selective named-field-construction
	v3 := i32x8{.$1 = 2, .$2 = 3}; // Using IDs for large vectors
	v4 := i32x3{10, 30, 40};       // Positional construction

	print($"v1 = {v1}\n");
	print($"v2 = {v2}\n");
	print($"v3 = {v3}\n");
	print($"v4 = {v4}\n");

This program will print these lines to the console:

v1 = (0, 0, 0, 0)
v2 = (3, 0, 5, 0)
v3 = (0, 2, 3, 0, 0, 0, 0, 0)
v4 = (10, 30, 40)

Important Note

When using vectors you gain free access to SIMD instructions. SIMD means Single Instruction, Multiple Data and its a very optimized way of doing operations, such as additions. For example, adding two i32x4 variables is just as fast as adding a single i32 variable. This makes Flint's vectors both very fast and very easy to use.