Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Variable Declaration and Assignment

SAIL distinguishes two kinds of mutable state: local variables declared inside function bodies and field variables declared inside contract bodies. Local variables exist only for the duration of a function call; field variables persist in contract storage across transactions.


Local Variable Declaration

A local variable is introduced with the let keyword inside a function body. The type annotation and the initialiser are both optional.

Declaration without annotation or initialiser

let x;

The compiler assigns a fresh type variable to x and infers its type from subsequent uses. The variable must be assigned before it is read; the compiler does not insert a default value.

Declaration with a type annotation

let bal : word;

The type is fixed to word at the point of declaration. The variable is still uninitialized; it must be assigned before use.

function loadBalance(account : word) -> word {
    let bal : word;
    assembly { bal := sload(account) }
    return bal;
}

Declaration with an initialiser

An initialiser provides a value at declaration time. The type may still be omitted and will be inferred from the initialiser expression.

let amount = 100;        // type inferred as word
let fee : word = 3;      // type annotation and initialiser together

Initialised declarations are useful when the right-hand side is an expression whose type would otherwise be ambiguous:

data Result = Ok(word) | Err(word);

function safeTransfer(from : word, to : word, amount : word) -> Result {
    let result = Result.Err(0);   // type inferred as Result from constructor
    let bal : word;
    assembly { bal := sload(from) }
    if (gte(bal, amount)) {
        result = Result.Ok(amount);
    }
    return result;
}

Assignment

Simple assignment

An assignment statement writes a new value to an existing variable or to a contract field. The left-hand side must be an lvalue: a name or an indexed expression.

x = expr;

The type of expr must match the declared type of x.

contract Vault {
    balance : word;

    function deposit(amount : word) -> () {
        let next : word;
        next = balance;
        balance = add(next, amount);
    }
}

Compound assignment

The += and -= operators combine a read, an arithmetic operation, and a write in a single statement.

balance += amount;   // equivalent to balance = balance + amount
balance -= amount;   // equivalent to balance = balance - amount

Compound assignment is most commonly used with contract fields:

contract ERC20 {
    totalSupply : word;
    feePool     : word;

    function mint(amount : word) -> () {
        totalSupply += amount;
        feePool     += div(amount, 100);
    }
}

Contract Field Variables

A field variable is declared inside a contract body with a mandatory type annotation and an optional initialiser. Fields are stored in contract storage and retain their values between calls.

contract Token {
    owner   : word;
    supply  : word;
    paused  : bool;
}

Fields are accessed and assigned by name from any function inside the same contract. A field cannot be accessed from a free function.

contract Token {
    supply : word;

    function mint(amount : word) -> () {
        supply += amount;
    }

    function totalSupply() -> word {
        return supply;
    }
}

Field initialisers

An optional initialiser sets the field's value at deployment time. It is evaluated once when the contract is deployed.

contract Token {
    supply : word = 0;
}

Contextual Assignment

The left-hand side of an assignment may be any expression that denotes an lvalue. When the expected type of the right-hand side is determined by the left-hand side, the contextual constructor shorthand .Constructor can be used on the right-hand side.

data Result = Ok(word) | Err(word);

function main() -> Result {
    let r : Result;
    r = .Ok(0);         // equivalent to Result.Ok(0)
    return r;
}

Conditional Statement

The if statement executes a block conditionally on a boolean expression. An optional else branch handles the false case.

if (condition) {
    // executed when condition is true
}

if (condition) {
    // true branch
} else {
    // false branch
}

Both branches must produce the same type if the if statement appears in a context where a value is expected. When used purely for side effects the types need only be consistent:

contract Token {
    paused : bool;

    function transfer(to : word, amount : word) -> () {
        if (paused) {
            assembly { revert(0, 0) }
        }
    }
}

Note The condition must be of type bool. SAIL does not implicitly convert word to bool. Use an explicit comparison (x != 0) when the condition originates from a word value.


For Loop

The for statement provides a C-style counted loop. Its header has three clauses separated by ;:

for ( ForInitStmt ; Condition ; ForPostStmt ) Body

The condition is any expression of type bool; the loop runs while it is true.

Initialisation clause

The init clause runs once before the first iteration. It may:

  • Declare a new local variable (typed or untyped, with or without an initialiser):

    for (let i = 0; i < 10; i = i + 1) { ... }
    for (let i : word; i < 10; i = i + 1) { ... }
    
  • Assign to an already-declared variable:

    let i : word;
    for (i = 0; i < 10; i = i + 1) { ... }
    
  • Use a compound assignment or a plain expression.

Post-iteration clause

The post clause runs after each iteration, before the condition is re-tested. It follows the same grammar as the init clause. A let binding introduced here creates a fresh variable scoped to the body of that iteration only:

for (i = 0; i <= 0; let j = 1) {
    s = j;    // j is in scope here and rebound on every iteration
    i = i + 1;
}

Complete examples

Accumulate a sum from 1 to 10:

import std.{Num, Add, Sub, Eq, Ord, Bounded, Typedef, le};

contract Sum {
    function main() -> word {
        let s = 0;
        for (let i = 1; i <= 10; i = i + 1) { s = s + i; }
        return s;    // 55
    }
}

Loop variable declared before the for:

contract Sum {
    function main() -> word {
        let i : word;
        let s = 0;
        for (i = 1; i <= 10; i = i + 1) { s = s + i; }
        return s;
    }
}

Loop variable shadows an outer declaration:

contract Shadow {
    function main() -> word {
        let i = 100;
        let s = 0;
        for (let i = 1; i <= 10; i = i + 1) { s = s + i; }
        // i is 100 here again
        return s;
    }
}

Nested if inside a for body:

contract ForInner {
    function main() -> word {
        let result = 0;
        for (let height = 0; height < 7; height = height + 1) {
            if (true) { result = height; } else {}
        }
        return result;
    }
}

Scope rules

A variable declared in the init clause is in scope for the condition expression, the post-iteration clause, and the entire body.

A variable declared in the post clause is in scope only for the body of the current iteration — it is re-bound at the start of each subsequent one.

The loop body is its own block; declarations inside it do not escape to the enclosing function.

Note The condition must be of type bool. SAIL does not implicitly convert word to bool. Use an explicit comparison (le(i, 10) or i <= 10) when comparing integer counters.


Expression Statements

Any expression may appear as a statement. The expression is evaluated for its side effects and the result is discarded. This is the standard way to call a function whose return type is ().

function emitTransfer(from : word, to : word, amount : word) -> () {
    assembly {
        mstore(0x00, amount)
        log3(0x00, 0x20, 0xddf252ad, from, to)
    }
}

function main(to : word, amount : word) -> () {
    emitTransfer(caller(), to, amount);    // expression statement: result () is discarded
}

Scope and Shadowing

Local variables are in scope from their declaration to the end of the enclosing block. A variable declared in an inner block shadows an outer declaration of the same name for the duration of that block.

function computeFee(amount : word) -> word {
    let fee : word = 1;
    {
        let fee : word = div(amount, 100);   // shadows outer fee inside this block
    }
    return fee;                              // refers to the outer fee; returns 1
}

Note The compiler uses unique identifiers internally, so shadowing is safe and does not cause name collisions in the generated code.