Types

Every value in Rux has a type, fixed at compile time. The type decides what the value can hold, how much storage it takes, and which operations apply to it. There are no implicit conversions that can change a value: the few that happen on their own only widen, and every other conversion is written with as.

Kinds of type

KindExamplesDescribed in
Integerint, uint8, int128Integers
Floating-pointfloat64, float32Floating-Point
Booleanbool, bool32Booleans
Characterchar, char8, char16Characters
Textchar8[..], char16[..]Text
Arrayint[4], float64[3][3]Arrays
Sliceint[..], var int[..]Slices
Tuple(int, char8[..])Tuples
Rangeint..int, int..=int, ..Ranges
Reference&T, &var TReferences
Pointer*T, *var T, *opaquePointers
OptionalT?Optionals
FallibleT ! E, ! EErrors
Sumint | boolSums
Functionfunc(int) -> intFunction types
Structstruct Point { … }Structs
Enumenum Color { … }Enums
Variantvariant Shape { … }Variants
Unionunion Word { … }Unions
Interfaceinterface Shape { … }Interfaces
Generic parameterT in func First<T>(items: T[..]) -> TGenerics

The integer, floating-point, boolean and character types are the primitive types. Their names are predefined, not keywords, and the Primitive Types appendix lists all of them, with sizes. Text is not a separate type: a string is a slice of characters.

Two more types have no family of their own: opaque, the unknown pointee of an untyped pointer *opaque, and (), the unit type of a function that returns nothing.

Type expressions

Type        ::= SumType ( '!' SumType )?
              | '!' SumType
SumType     ::= RangeType ( '|' RangeType )*
RangeType   ::= PostfixType ( ( '..' | '..=' ) PostfixType? )?
              | ( '..' | '..=' ) PostfixType?
              | 'var' PostfixType
PostfixType ::= PrimaryType ( '?' | '[' ']' | '[' Expression ']' | '[' '..' ']' )*
PrimaryType ::= Path TypeArguments?
              | '*' 'var'? PostfixType
              | '&' 'var'? PostfixType
              | 'func' '(' … ')' ( '->' Type )?
              | '(' Type ( ',' Type )* ')'
              | '(' ')'

The forms bind in this order, tightest first:

  1. Postfix suffixes, applied left to right: ? optional, [N] array, [..] slice, and [], the flexible array tail a struct's last field may have (see Memory Layout).
  2. Range bounds T..T, T..=T, and the var of a writable slice var T[..].
  3. Sum A | B.
  4. Fallible T ! E — one ! per type; a leading ! E is a fallible with no success value.

The pointer and reference forms take a postfix type, so *int? is a pointer to an optional, *(int?).

WrittenMeans
int32?[..]A slice of optional int32
int32[..]?An optional slice of int32
int[3][2]An array of 2 elements, each an int[3]
int??An optional of an optional int
A | B ! E | F(A | B) ! (E | F)
T ! (E?)A fallible whose error is an optional — grouped
A | (B?)A sum with an optional member — grouped

An optional member of a sum and an optional error of a fallible must be written in parentheses, since A | B? could mean either A | (B?) or (A | B)?. Ungrouped, they are error: an optional sum member must be grouped and error: an optional error type must be grouped.

variant ParseError {
    Empty,
    Bad(char8)
}

func Double(x: int) -> int {
    return x * 2;
}

func Check(text: char8[..]) -> ! ParseError {
    if text.length == 0 {
        fail ParseError::Empty;
    }
}

func Main() -> int {
    let grid: int[3][2] = [[1, 2, 3], [4, 5, 6]];
    let pair: (int, char8[..]) = (1, "one");
    let span: int..int = 0..10;
    let maybe: int? = none;
    let either: int | bool = 7;
    let op: func(int) -> int = Double;
    var storage: int[4] = [1, 2, 3, 4];
    let view: var int[..] = storage[..];
    let address: *int = @grid[0][0];
    let pointerToOptional: *int? = null;
    view[0] = 10;
    return grid[1][2] + op(pair.0);   // 6 + 2
}

Type identity

Two type expressions denote the same type when they are spelled the same after aliases are resolved. A type alias is another name for its target, never a new type. Each struct, enum, variant, union and interface declaration, on the other hand, introduces a type distinct from every other, even one with identical fields. A generic type is a different type for each set of type arguments: Box<int> and Box<bool> are unrelated.

int and int64 are distinct types — a function can be overloaded on them — that convert to each other implicitly, because int is 64 bits wide on every supported target. The same holds for uint and uint64.

Implicit conversions

A value converts to another type without as only when no value can change:

FromToExample
An integerA wider integer of the same signednessint8 → int32, uint32 → uint
int / uintint64 / uint64, and back
float32float64
char32char64
Any boolean widthAny other boolean widthbool → bool32
T[N]T[..]An array viewed as a slice
var T[..], *var T, &var TT[..], *T, &TDropping write access
Any pointer*opaque
TT?, or a sum that has T as a memberSee Optionals and Sums

Everything else needs as: narrowing, changing signedness, crossing between integers, floats, characters and booleans, and float64 to float32. Each family page lists its conversions. Unsuffixed integer literals are the one flexible case: they take the type their context needs, as Literals describes.

Two integer operands of one signedness meet at the wider type, as an assignment would widen the narrower one. Operands of different signedness have no common type:

let u: uint64 = 3;
let s: int64 = -5;
let mixed = u + s;
error: operator '+' cannot combine left operand 'uint64' with right operand 'int64'

A result that is wider than its destination is not narrowed back: with small: int32 and big: int64, let t: int32 = small + big; is error: cannot assign 'int64' to 'int32'.

Size and alignment

sizeof(T) and alignof(T) give a type's storage size and alignment in bytes, as uint values computed at compile time. Every primitive's size is fixed by its name — int and uint are 8 bytes on every supported target — and Memory Layout describes how compound types are laid out.

See also