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.
Please note that they are not yet available on the mainnet, but are planned to be released in the future.
You can find the documentation of the XVM (opens in a new tab) 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 function—this serves as the main entry point for 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_temporary
is 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 (likefor
loops) 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 (true
orfalse
).string
: UTF-8 text type.null
: Special null value (only valid for optional types).
Composite Types
- Arrays: Denoted by
T[]
whereT
is the element type. Multidimensional arrays becomeT[][]
, etc. - Optional: Denoted by
optional<T>
; can hold a value of typeT
ornull
. - Map: Denoted by
map<K, V>
; a dictionary from keysK
to 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_variables
is enabled, but can be permitted if that feature is disabled in the parser.
Constants
- Declared with
const
and 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.field
orexpr.methodCall(...)
. - Indexing:
myArray[2]
. - Casts:
myVal as u32
. - Ternary:
cond ? exprIfTrue : exprIfFalse
.
Operators
- Arithmetic:
+
,-
,*
,/
,%
,**
(power). - Bitwise:
&
,|
,^
,<<
,>>
. - Comparisons:
==
,!=
,<
,<=
,>
,>=
. - Logical:
&&
,||
,isnot
(unaryisnot expr
flips a boolean). - Assignment:
=
,+=
,-=
, etc.
For example,x += 5
is 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
; there can be at most one or a limited number of them in a contract (depending on the environment). Typically, an entry function returns u64
, though the parser can be configured to allow other types or no return type.
entry main() -> u64 {
return 0
}
Hook Functions
Marked by hook
; these are special callbacks or event handlers invoked by the system. The environment typically imposes constraints on their name, parameters, and return types:
hook onDataReceived(data: u8[]) {
// handle data
}
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 Accumulator
with a single field,sum
. - We define an
enum Operation
with two variants:Add
andReset
. - We create a method
doOperation
onAccumulator
that takes anOperation
and returns a newAccumulator
. - The
entry main
function is the main entry point, returning au64
.