Skip to content

xelis-project/xelis-vm

Repository files navigation

XELIS VM

XVM (XELIS Virtual Machine) is a fully customizable VM proposed with its suite including Lexer, Parser, Assembler, Compiler and the Virtual Machine.

The name of the language used is Silex, which its syntax is inspired by Rust.

Unlike other VMs, XVM is designed to be fully customizable (including the default std!), allowing you to define your own types, functions, structures, enums. It is also designed to directly include primitive types.

XVM is designed to be fully deterministic, stable and correctly sandboxed to ensure performance and security without limiting its usage. It is also compatible with async operations for better integration within your systems.

All the verifications are mainly made at the level of the Parser and compiler to check the conformity of the code to ensure correct code compilation into op codes program. Nonetheless, there is still a Validator available next to the VM to ensures types and programs are still well-formed against a required Environment before any execution.

Thanks to this fully customizable Environment, you can define your own types, functions, structures, enums, and even override the default std functions if required.

For more flexibility, an Opaque type is also available to handle your whole logic in Rust directly increasing performances and allowing to expose a clean API.

File extension is .slx for the source code.

Tasks

Here is the list of tasks left ordered by their priority to have a good MVP (Minimum Viable Product):

  • Improve Parser Parser is currently operating in a full recursive mode, which can create a stackoverflow in case of a too complex/deep expression to build. Rewriting the key parts to operate in an iterative way would prevent any stack overflow.

  • Imports Allow to import other files to have a better code organization.

  • ABI Generating a "mapper" file to easily link entry functions (name, parameters), enums, structs.. to the corresponding ids on chain. This will allow easy plugin-in with future XSWD lib and dApps development.

  • VM optimizations The faster the VM is, the more we can have reduced cost for running a Smart Contract.

Crates

  • vm: Virtual Machine to execute a (op-code) compiled program.
  • assembler: Assembler to convert an source code of raw instructions into a program.
  • ast: Convert a source code file into a AST (Abstract Syntax Tree) to create a program of statements/expressions.
  • compiler: Compiler to convert an AST (Abstract Syntax Tree) program into an op-code program.
  • parser: Parser to convert a list of tokens into an AST (Abstract Syntax Tree) program.
  • lexer: Lexer to convert a source code into a list of tokens.
  • environment: Environment types used by the VM, Compiler and Parser to support a program based on the std provided.
  • builder: Environment builder to create your own std, with a (limited) default one included.

Types

The different primitive types are:

  • u8 (unsigned 8 bits)
  • u16 (unsigned 16 bits)
  • u32 (unsigned 32 bits)
  • u64 (unsigned 64 bits)
  • u128 (unsigned 128 bits)
  • u256 (unsigned 256 bits)
  • bool
  • string
  • struct
  • enum variants
  • optional<T> where T is another type (it allow the value to be nullable)
  • range<T> where T is a number type (it allow to iterate over a range of values in a foreach, or have some functions like contains)
  • map<K, V> where K is a key type and V is a value type (it allow to have a key-value store)
  • bytes is a raw data type allowing to store any kind of data (like images, files..) but also to do faster computations. Its representing a Vec<u8> efficiently.

Arrays of any type are also supported, but they must contain only one type of value (example: u64[] and with multi-depth too).

OpCodes

The opcodes are the instructions that the VM will execute. They are generated by the compiler and are executed by the VM. See the opcodes.md file for more information.

Documentation

the semicolon is optional, thus can be added if desired without any difference in the code.

Recursive functions are allowed, but limited to a configurable depth.

A environment system is completely customizable to set your own native functions. This helps to manage exactly what a program can interact with. Custom structs are also available.

Numbers

An error will be returned by the interpreter if an overflow is detected without causing a panic.

Rules

  • The value must be greater than or equal to 0.
  • You can put _ (underscore) for a better readability.
  • If no type is specified on the value, then u64 will be the default.
  • Array indexes are u32 types.
  • You can precise the type by adding u8, u16, u32, u64, u128 or u256 after the value.

Examples

let my_u8: u8 = 10 let my_u16: u16 = 70 let my_u32: u32 = 999 let my_int: u64 = 25655 let my_u128: u128 = 100_000_000u128 let my_u256: u256 = 100_000_000u256

Each type can be casted into another type, if an overflow is detected, the value will be truncated.

let my_u8: u8 = 255 let my_u16: u16 = my_u8 as u16

Also, each type have a min and max value that can be used.

let min: u8 = u8::MIN let max: u8 = u8::MAX

Variable

for constant variable, it must be declared outside a function, with const keyword.

Rules

  • Every variable must be declared with let or const keyword.
  • Variable name must alphanumeric characters.
  • Must provide value type.
  • If no value is set, null is set by default.

Examples

const hello: string = "hello" ... let world: string = "world"

Casting

Values of built-in types can be casted into other built-in types easily using the keyword as. In case of an overflow, no error will be returned, but the value will be truncated.

Rules

  • Both value types must be a built-in type.

Examples

let id: u128 = 1337 let b: u8 = id as u8 // id_str equals "255" due to the truncation let id_str: string = id as string

Import

Instead of having one file with all your code, you can have multiple files that will be compiled into one final program.

Rules

  • Have a unique alias if set
  • No circular import
  • ends with .xel if its a local import

Examples

math namespace

import "math.xel" as math; ... math.sum(a, b)

no namespace:

sum(a, b)

Function

entry function is a "public callable" function and must return a u64 value.

Rules

  • Must starts with fn or entry keyword.
  • Signature is based on function name and parameters.
  • For type functions, the type must not be primitive.
  • Recursive functions are allowed.

Examples

entry foo() { ... } fn foo() { ... } fn foo() -> u64 { ... } fn foo(a: u64, b: u64) { ... } fn (f Foo) bar() { ... }

Structure

A structure can contain other structures.

Rules

  • The name must be unique.
  • Name should start with a uppercase letter.
  • Only letters are allowed in name.
  • The last field does not need a comma.

Examples

struct MyStruct { message: string, value: u64 }

Enum

An enum is a type that can have multiple variants.

Rules

  • The name must be unique.
  • Name should start with a uppercase letter.
  • Variants must be unique.
  • Each variants can contains fields or not.

Examples

enum MyEnum { A, B, C { value: u64 }, D { name: string, value: u64 } }

Optional

An optional type is a type that can be null.

Rules

  • The type must be specified.
  • The value can be set to null.

Examples

let my_optional: optional<u64> = null ... let opt: optional<string> = "Hello World!" let s = opt.unwrap()

Range

A range is a type that can be used to iterate over a range of values.

Rules

  • The type must be specified and be a number type.
  • The start and end values must be of the same type.
  • The end value must be greater than the start value.

Examples

let my_range: range<u64> = 0..10 let _: bool = my_range.contains(5)

Map

A map is a key-value store where the key and value can be of any type based on the declaration. It is backed by a HashMap.

Rules

  • The key and value types must be specified.
  • Key type can't be a map.

Examples

let my_map: map<string, u64> = {"hello": 10, "world": 20} my_map.insert("foo", 30) my_map.remove("hello")

Ternary

Rules

  • A bool condition is required.
  • The two values that can be returned must be of the same type.

Examples

let score: u64 = is_winner() ? 20 : 0

Negate operator

Rules

  • A bool condition is required after it.

Examples

let negative: bool = !condition

Array

Rules

  • All values must be of the same specified type.

Examples

let array: u64[] = [10, 20, 30, 40] ... let dim: u64[][] = [[34, 17], [8, 14], [0, 69]]

If

Rules

  • Have a bool condition.

Examples

if condition {	... } if (i > 20 && i != 25) || i == 0 {	... }

Else

Rules

  • It must be preceded by an if condition.

Examples

else {	... }

Else if

Rules

  • It must be preceded by an if or an else if condition.
  • Have a boolean condition.

Examples

else if condition {	... } else if my_struct != null {	... }

While

Rules

  • Have a boolean condition.

Examples

while condition {	... }

Foreach

Rules

  • Have the name of a variable.
  • Have an array to go through

Examples

foreach val in values {	... }

You can also do on specific ranges:

foreach i in 0..10 {	... }

For

Rules

  • Have the name of a variable.
  • Have a boolean condition.
  • Have an assign operator.

Examples

for i: u64 = 0; i < 10; i += 1 {	... }

Break

Rules

  • Must be in a loop (foreach, for, while).

Examples

while condition { if i % 10 == 0 { break; }	... }

Continue

Rules

  • Must be in a loop (foreach, for, while).

Examples

while condition { if i % 10 == 0 { continue; }	... }

Return

Rules

  • Must not have any code after.
  • If the function returns a value, the return must return a value.

Examples

fn foo() -> string { return "Hello World!" } fn bar() { if condition { return	} foo() }

Scope

Allows you to isolate a part of the code / variables created.

Rules

  • No specific rules.

Examples

{	... }

Releases

No releases published

Packages

 
 
 

Contributors

Languages