ferrule documentation[styled mode]
specrfcshome

declarations and bindings

Status: α1


declarations and bindings

this covers how you declare variables and what happens when you assign them.

immutability by default

the default binding is const:

const pageSize: usize = layout.page_size();

const bindings can't be reassigned. this is the common case and should be your default.

mutable bindings

use var when you need mutation:

var counter: u32 = 0;
counter = counter + 1;

type inference

ferrule infers types when unambiguous:

const x = 42;        // i32 (default integer)
const y = 3.14;      // f64 (default float)
const s = "hello";   // String
const b = true;      // Bool

annotation required when ambiguous or non-default:

const port: u16 = 8080;      // could be many int types
const ratio: f32 = 3.14;     // need f32 not f64
const items: Vec<User> = vec.new();  // empty collection needs type

function results always need annotation:

const result = compute();       // error: can't infer, annotate
const result: Data = compute(); // ok

literal type preservation

const preserves literal types, var widens:

const x = 42;     // type is literal 42
var y = 42;       // type is i32 (widened)

this matters for const generics and refinement types.

no implicit coercion

ferrule never converts types implicitly:

const a: u16 = 100;
const b: u32 = a;       // error: u16 is not u32

const b: u32 = u32(a);  // ok: explicit conversion

this applies to everything:

move semantics

when you assign a move type, ownership transfers:

const s: String = "hello";
const t = s;      // s is moved to t
// s is now invalid

this is not a copy. the data isn't duplicated. s becomes unusable after the assignment.

for copy types, assignment duplicates:

const a: i32 = 42;
const b = a;      // a is copied to b
// both a and b are valid

see types.md for which types are copy vs move.

use after move

using a moved value is a compile error:

const data: String = "hello";
const other = data;  // move

io.println(data);  // error: data was moved

the compiler tracks which variables have been moved and errors if you try to use them.

conditional moves

if a value might be moved in one branch, it's invalid after the conditional:

const data: String = "hello";

if condition {
    consume(data);  // moves data
}

use_data(data);  // error: data might have been moved

the safe pattern is to move in all branches:

const data: String = "hello";

if condition {
    consume(data);
} else {
    other_consume(data);
}
// data is invalid on all paths, which is fine

loop moves

you can't move the same variable in a loop:

const data: String = "hello";

for i in 0..3 {
    process(data);  // error: can't move in loop
}

use clone if you need to pass owned data in each iteration:

for i in 0..3 {
    process(data.clone());  // explicit copy each time
}

by-reference parameters

use inout for by-reference mutation:

function bump(inout x: u32) -> Unit { 
  x = x + 1; 
}

var counter: u32 = 0;
bump(inout counter);
// counter is now 1

rules:

destructuring

you can destructure records and arrays:

const User { name, age } = user;  // moves both fields out
// user is now fully invalid

const [first, second, ...rest] = items;  // array destructuring

partial moves (moving just one field) are not allowed. if you need one field, destructure the whole thing.

summary

keywordmeaning
constimmutable binding, preserves literal types
varmutable binding, widens literal types
inoutby-reference parameter
assignmentbehavior
copy typeduplicates value, both valid
move typetransfers ownership, original invalid