Introduction
Smart Contracts can be programmed using our own interpreted language running in a Virtual Machine, allowing safe and secure execution of code on the blockchain.
Smart Contracts are a way to automate the execution of a contract, without the need of a third party. They are self-executing contracts with the terms of the agreement directly written into lines of code.
A lot of use cases can be found for Smart Contracts, such as:
- Decentralized applications (dApps)
- Tokenization of assets
- Voting systems
- Supply chain management
- etc.
You can find the documentation of the XVM below.
Silex Lang
Silex is a statically typed programming language powered by XVM (XELIS Virtual Machine) with syntax that should feel familiar to those experienced with modern C-family languages.
XVM is a fully customizable VM built from scratch in Rust providing a robust & deterministic sandboxed environment.
This guide aims to describe its major features, from syntax to data structures and control flow, so that you can effectively write contracts in Silex.
Silex code typically consists of:
- Constant declarations (
const) - Functions (
fn,entry,hook) - Struct and Enum definitions
- Imports for referencing other files
- Statements (control flow, variable assignments, loops, etc.)
Every Silex contract can define several entry functions. These are the callable entry points used by external invocations.
Lexical Elements
-
Identifiers:
- Must start with an alphabetic character (
[A-Za-z]) and can contain letters, digits, or underscores. - Examples:
myVar,Address1,_temporary(although_temporaryis valid, a single_is treated as an ignored placeholder variable).
- Must start with an alphabetic character (
-
Keywords:
Common keywords includelet,const,fn,enum,struct,entry,hook,if,else,while,for,foreach,in,break,continue,return, etc. -
Semicolons:
Silex has minimal semicolon usage. A newline often denotes the end of a statement, but semicolons may still appear to separate statements if desired. Some syntax forms (likeforloops) explicitly use semicolons. -
Braces & Parentheses:
{ ... }typically denotes blocks or type constructors.( ... )encloses function parameters and subexpressions.[ ... ]encloses array elements or indices.
Data Types
Primitive Types
u8,u16,u32,u64,u128,u256: Unsigned integer types of various bit-widths.bool: Boolean type (trueorfalse).string: UTF-8 text type.null: Special null value (only valid for optional types).
Composite Types
- Arrays: Denoted by
T[]whereTis the element type. Multidimensional arrays becomeT[][], etc. - Optional: Denoted by
optional<T>; can hold a value of typeTornull. - Map: Denoted by
map<K, V>; a dictionary from keysKto valuesV. The key must not be anothermap. - Range: Denoted by
range<T>; a half-open or fully open range from one value to another of typeT(usually a numeric type). - Struct: A user-defined record type (see Structs).
- Enum: A user-defined discriminated union type (see Enums).
Variables and Constants
Variables
- Declared with
let. - Must specify a type and (usually) an initializer.
let x: u64 = 10
let greetings: string[] = ["hello", "world"]- If a type is optional, you can omit the assignment and it will default to
null:
let maybeValue: optional<u64> // defaults to null- Silex disallows “shadowing” by default if
set_shadowing_variablesis enabled, but can be permitted if that feature is disabled in the parser.
Constants
- Declared with
constand must be uppercase. - Must have a compile-time–resolvable expression:
const MAX_SIZE: u64 = 100- Named
_is disallowed for constants.
Expressions
Expressions in Silex include:
- Literals: e.g.,
42,true,"hello",null. - Variable references:
myVar. - Array/map constructors: e.g.,
[1, 2, 3],{ key: "value" }. - Struct/enum constructors: e.g.,
MyStruct { fieldName }orMyEnum::VariantName { ... }. - Operators (arithmetic, logical, bitwise).
- Function calls:
fnName(arg1, arg2). - Member access:
expr.fieldorexpr.methodCall(...). - Indexing:
myArray[2]. - Casts:
myVal as u32. - Ternary:
cond ? exprIfTrue : exprIfFalse.
Operators
- Arithmetic:
+,-,*,/,%,**(power). - Bitwise:
&,|,^,<<,>>. - Comparisons:
==,!=,<,<=,>,>=. - Logical:
&&,||,isnot(unaryisnot exprflips a boolean). - Assignment:
=,+=,-=, etc.
For example,x += 5is equivalent tox = x + 5.
Operator precedence is generally “C-like,” with power (**) and unary operators near the top, arithmetic/bitwise in the middle, and logical last.
Flow Control
if / else
if condition {
// ...
} else if anotherCondition {
// ...
} else {
// ...
}Loops
while
while x < 10 {
x += 1
}for (C-style)
for i: u64 = 0; i < 10; i += 1 {
// use i
}This syntax includes an initializer (i: u64 = 0), a condition (i < 10), and an increment expression (i += 1).
foreach
foreach element in myArray {
// element is the array’s element type
}break / continue
Use break to exit a loop immediately, and continue to skip to the next iteration.
return
Returns from the current function, optionally with an expression if the function has a return type. If a function declares a return type, its final statement must ultimately return a value.
Functions
Functions start with fn, entry, or hook. They have a name, an optional list of parameters, and an optional return type. Entry and Hook functions have special roles in a smart contract context.
fn add(a: u64, b: u64) -> u64 {
return a + b
}Entry Functions
Marked by entry; they are the main entry points for external calls to the contract.
When an entry has no explicit return type, it uses the default u64 exit code. Return 0 for success, or any value greater than 0 to report a contract-defined error code. This is useful when a contract wants callers and indexers to distinguish exact failure reasons without parsing text.
entry main() {
return 0
}If you add an explicit return type, the entry returns data instead of only the default exit code. The encoded return value can be up to 256 bytes and is stored as the execution exit_value in the contract logs.
entry greeting(name: string) -> string {
return "Hello, " + name
}If execution raises an error, for example because require(condition, "message") fails, the error is also stored in the contract logs.
entry checked(value: u64) {
require(value > 0, "value must be positive")
return 0
}Hook Functions
Marked by hook; these are system-invoked functions with special lifecycle behavior.
Currently, constructor() is the only hook. It is called once during contract deployment, before the contract is available for normal entry invocations. After deployment finishes, the constructor cannot be called again by users, other contracts, entries, or the contract itself.
Use constructor() for one-time setup such as initializing storage, recording the owner, or preparing configuration needed by later entries. It must explicitly return u64; 0 indicates success, and any value greater than 0 can be used as a deployment-time error code. If the contract defines a constructor, the deployment transaction must include an invoke payload with a max_gas limit so the constructor can execute.
hook constructor() -> u64 {
let storage: Storage = Storage::new();
storage.store("owner", Transaction::current().source());
return 0;
}Methods on Structs/Enums
A function can be declared as a “method” by specifying (varName Type) before the function name:
fn (p Person) greet() -> string {
return "Hello, " + p.name
}This is akin to an instance method in other languages: p is the struct instance. You can only declare methods for a type that is locally defined (e.g. a struct or enum you own).
Structs
Structs are user-defined composite types with named fields. By convention, struct names start with uppercase letters.
struct Person {
name: string,
age: u64
}To instantiate:
let p: Person = Person { name: "Alice", age: 30 }If you have a local variable name that matches the field name, you can shorthand it as Person { name }.
Enums
Enums represent discriminated unions (sum types). Each variant may have zero or more fields:
enum Result {
Ok { value: u64 },
Err { message: string }
}Create an instance:
let res: Result = Result::Ok { value: 123 }Check a variant with pattern matching in a future extension or by calling environment-defined methods to see which variant is active.
Imports
Use import "someFile.slx" to load external code. The parser enforces certain rules like preventing .. and absolute paths:
import "helpers.slx"(Alias syntax or advanced scoping might be added later.)
Example Program
Below is a complete (albeit simple) Silex contract that demonstrates constants, variables, loops, a struct, an enum, and an entry function.
const MAX_COUNT: u64 = 5
struct Accumulator {
sum: u64
}
enum Operation {
Add { value: u64 },
Reset
}
fn (acc Accumulator) doOperation(op: Operation) -> Accumulator {
match op {
Operation::Add { value } => {
// If pattern matching is not directly supported yet,
// we can parse the fields in future expansions.
Accumulator { sum: acc.sum + value }
},
Operation::Reset => Accumulator { sum: 0 }
}
}
entry main() -> u64 {
// Create an instance of Accumulator
let acc: Accumulator = Accumulator { sum: 0 }
// Perform operations
let updated: Accumulator = acc.doOperation(Operation::Add { value: 10 })
let resetAcc: Accumulator = updated.doOperation(Operation::Reset)
// Use a for-loop up to MAX_COUNT
let mut result: u64 = 0
for i: u64 = 0; i < MAX_COUNT; i += 1 {
result += i
}
// Return the final result
return result + resetAcc.sum
}Explanation:
- We declare a constant
MAX_COUNT. - We define a
struct Accumulatorwith a single field,sum. - We define an
enum Operationwith two variants:AddandReset. - We create a method
doOperationonAccumulatorthat takes anOperationand returns a newAccumulator. - The
entry mainfunction is the main entry point and stores a typedu64exit value.