Bindings

A binding gives a name to a value. Rux has three kinds, each introduced by its own keyword:

KeywordBindsCan changeComputedPage
letan immutable run-time valuenoat run timethis page
vara mutable run-time valueyesat run timethis page
consta compile-time valuenoby the compilerConstants
binding     = ("let" | "var") pattern [":" type] [initializer] ";"
initializer = "=" expression
            | "<-" expression

The pattern is usually a single name. It can also take a tuple or a structure apart — see Destructuring.

let answer = 42;
var counter = 0;
counter += 1;

let

let binds a value that never changes. The name keeps the value it was given for as long as it is in scope:

let limit: int32 = 10;
limit = 11;
error: cannot modify immutable variable 'limit'
  help: declare 'limit' with 'var' to make it mutable

Because a let can never be assigned later, it must be initialized where it is declared. let total: int32; is error: immutable variable requires an initializer.

Prefer let. A name that cannot change is one less thing for the reader to track, and the compiler rejects an accidental write instead of letting it through.

var

var binds a value that may be replaced. Assignment, the compound operators and ++/-- all need a var (or another mutable place, such as a field of one):

var score: int32 = 10;
score = 12;
score += 5;
score++;

A var may also be declared without a value and assigned later. Until then it holds nothing, and the compiler makes sure no path reads it too early — see Initialization.

Types and inference

With an initializer and no annotation, the binding takes the initializer's type. Literals have a default type:

InitializerInferred type
42, 3000000000int
1.5float64
'A'char32
truebool
"text"char8[..]
7u8, 2.5f32, c8'A'the suffix or prefix's type

An annotation, : Type, chooses the type instead. An unsuffixed integer literal then takes the annotated type, provided its value fits:

let small: uint8 = 200;
let wide: int64 = 5;

let small: uint8 = 300; is error: integer literal is out of range for type 'uint8'.

A binding's type is fixed once it is declared. A value of another type is not converted to fit, with one exception: an integer widens to a wider integer of the same signedness, because no value can change on the way. Every other change of type is written with as:

let narrow: int32 = 7;
let widened: int64 = narrow;
let converted = narrow as float64;

The reverse, let back: int32 = widened;, is error: cannot assign 'int64' to 'int32'.

= and <-

An initializer is written with = or <-, and the two mean different things:

FormMeaning
let b = a;copy: b gets its own copy, and a is unchanged
let b <- a;move: the value is handed to b, and a may not be read again
let first = Buffer { size: 4 };
let second <- first;
PrintLine("{}", second.size);

Reading first after the move is error: value 'first' is used after it was moved. A type that cannot be copied, such as one that owns a resource, can only be bound with <- from a named source; a freshly made value, such as the result of a call, needs no <-. The full rules are in Copy and move.

Mutability

Mutability belongs to the binding and reaches all the way into the value. A let structure cannot have any field changed, at any depth; a var one can have every field changed, or be replaced whole:

let fixed = Point { x: 1, y: 2 };
var moving = Point { x: 1, y: 2 };
moving.x = 5;
moving = Point { x: 0, y: 0 };

fixed.x = 5; is error: cannot modify immutable variable 'fixed'. There is no per-field opt-out in either direction. The same holds for tuples and fixed-size arrays.

A function changes a caller's value only through a writable reference, &var T, or a writable pointer, *var T — see References and Pointers. A parameter itself is always immutable; a function that needs a mutable copy moves the argument into a local, as in var local <- value;.

Scope

A binding is visible from its declaration to the end of the block that contains it. Declaring the same name twice in one block is an error:

let total = 1;
let total = 2;
error: variable 'total' is already declared in this scope

Each name a destructuring pattern declares counts separately, so let (p, p) = (1, 2); is refused the same way.

A block nested inside another — the body of an if or a loop — may declare a name that is already used outside it. The inner binding hides the outer one until the block ends.

Reusing an outer name.
rux 0.4.0 does not yet keep a hiding binding apart from the one it hides: after the inner block, the outer name can read the inner value. Until that is fixed, give a binding in a nested block a name of its own.

Discarding a value

_ is not a name. let _ = value; evaluates value and throws it away, and nothing can read _ afterwards — see Destructuring.

See also