Language Basics

You write one contract per file. SCL contracts have constants to pin values, fields to hold state, functions to move that state forward, and events to tell the outside world what happened. Always deterministic, never random or unpredictable, so any node can run the same functions and see the same outcome.

Structure

contract Minimal {
    // constants
    const STEP: u64 = 1;

    // fields
    @writeable_by(init, bump)
    counter: u64;

    // events
    event Bumped(new_value: Int);

    // functions
    fn init(x: u64) {
        counter = x;
    }

    fn bump() {
        counter = counter + STEP;
        emit Bumped(counter);
    }
}

@writeable_by is enforced at code generation time. If a function not listed attempts to write a protected field, the assignment is skipped. No runtime error is raised. Fields without the attribute are read-only from the contract body.

Types

Supported type syntax in the parser today:

  • Scalars: Int, u64, u8, Bool, Str, String, Bytes, PubKey
  • Collections: List<T>, Map<K, V> / HashMap<K, V>, tuples (T1, T2, ...)
  • Option and Result: Option<T>, Result<T, E>
  • Literals: numbers, strings, booleans (true, false), byte literals (b"0x..."), lists [1, 2]

All arithmetic is 64-bit integer arithmetic. There are no floating-point types.

Booleans compile into Int at runtime: one for true, zero for false. Comparison and logical operators return 0 or 1, so branch on those values directly. Functions signal success or failure the same way: return 1 for success and 0 for failure.

Control flow

  • if, else if, else:
fn guard(n: u64) {
    if (n == 0) {
        PRINT("zero");
    } else {
        PRINT("nonzero");
    }
}
  • for over a collection:
fn tally(entries: List) {
    let mut total = 0;
    for e in entries {
        total = total + e;
    }
    PRINT("total:", total);
}

There is no while loop. Iteration is always over a concrete collection, so every loop is bounded by construction. Recursion is syntactically possible but capped hard by the VM's 64-frame call-depth limit.

Variables and returns

fn example() {
    let fixed = 10;        // immutable local
    let mut running = 0;   // mutable local
    running = running + fixed;
    return running;        // or plain `return;`
}

Operators

Arithmetic: + - * /
Comparison: == != < > <= >= (work on integers, strings, and bytes; result is 0 or 1)
Logical: && || ! (zero is false, non-zero is true)

Events

Declare events at contract level and emit them from function bodies:

contract Registry {
    event Registered(name: Str, owner: Str);

    fn register(name: Str, owner: Str) {
        MAP_SET("names", name, owner);
        emit Registered(name, owner);
        return 1;
    }
}

Events appear in your contract's ABI and are stored by nodes, queryable per contract over the node API. See Events for the full picture.

What SCL deliberately leaves out

  • Unbounded loops (while, do-while)
  • Floating-point arithmetic
  • Randomness and system time
  • Dynamic dispatch, function pointers, and code generation
  • Imports and modules: one contract per file
  • Exceptions: error handling is by return codes (0/1)

These aren't missing features; they're the reason every SCL contract is cheap to verify and impossible to trap a node with.

Next Steps

Move to Built-ins for persistent maps, sets, cross-contract calls, and context helpers you can call today.