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");
}
}
forover 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.