XenonCode - Documentation

XenonCode is a lightweight programming language designed as high level scripting within games for virtual computing devices (ie: a programmable computer within a game).

Capabilities

Sample code

include "my_functions.xc"

; Declare global storage
storage var $myStorageVar : number
storage array $myStorageArray : number

; Declare global variables
var $myVar = 0
var $myVar2 = "Hello World"
var $myVar3 : number
var $myVar4 : text
array $myArray : number
array $myArray2 : text

; Declare a user-defined function that changes the values of $myVar and $myVar2
function @myFunction($arg1 : number, $arg2 : text)
    $myVar = $arg1
    $myVar2 = $arg2

; Declare a user-defined function that returns a value
function @myFunction2($arg1 : number, $arg2 : text) : number
    return $arg1 + $arg2.size

init
    ; Call a user-defined function
    @myFunction(1, "Hello")

    ; Call a trailing function
    $myVar.@myFunction2("Hey")

    ; Add values to an array
    $myArray.append(1)
    $myArray.append(5)
    $myArray.append(0)

    ; Sort an array
    $myArray.sort()

    ; Iterate over an array
    foreach $myArray ($item)
        $myVar3 += $item

    ; Iterate over an array with index
    foreach $myArray ($item, $index)
        $myVar4 &= $index:text & ": " & $item:text & ", "

    ; Output to a virtual device (ie: a console at input port 0)
    output.0 ($myVar4)
    output.0 (text("Sum of values in the array: {}", $myArray.sum))
    output.1 ($myArray.0:text)
    output.1 ($myArray.1:text)
    var $index = 2
    output.1 ($myArray.$index:text)

    ; Repeat a statement three times
    repeat 3 ($i)
        output.1 ($i)

Types of developer

  1. User: The person who is using this language to write a script, typically a player in a game.
  2. Device: The implementation defining the capabilities and available functions, typically a specific type of programmable virtual device in a specific game.

Syntax

XenonCode is designed with a very basic syntax in mind and a very precise structure.

Types

XenonCode is a typed language, but only includes two generic types, as well as arrays of either, and implementation-defined objects.

Generic data types the user can declare:

A number variable is always a 64-bit floating point internally, but may also act as a boolean when its value is either 1 or 0 (true or false), although any value other than zero is evaluated to true.

text variables contain plain unicode text, altough their maximum size depends on the implementation.
A text literal is defined within double quotes " ".
To use double quotes characters within the text, you may duplicate them.
There is no other escape sequence mechanism. A backslash \ is simply part of the string, and the implementation may decide to use it for escape sequences.

var $myText = "Say ""Hello"" to this text"

Object types are for use by the implementation and are opaque to the user, meaning their structure is not necessarily defined, however the implementation may make availalbe some member functions for those objects to the user.

Even though this is a typed language, specifying the type is not needed when it can be automatically deduced by the compiler during initialization. The type should only be specified when there is no initialization.

All user-defined words must start with a prefix character:

Comments

Comments are lines that start with either ; or //
A code statement may also end with a trailing comment

Limitations

This language is designed to potentially be executed Server-side in the context of a multiplayer game, hence for security and performance reasons there are limits to what users can do.

Per-Virtual-Computer limitations

These are defined per implementation and may include multiple variants or be customizable by the user

Operation on data

Basic rules

Valid usage

XenonCode is designed to be compiled as byteCode which is very fast to parse by the host game at runtime.

Each line must begin with one of the following first words (with examples):

Global scope

Function body / Entry point

Reserved keywords

Since all user-defined words must start with either $ or @, there is no need for reserved words.
The implementation/device must take care of only defining function names and object types that do not conflict with built-in keywords for the version of XenonCode that they're using.

Declaring a constant

Constants are named values that should never change throughout the execution of a program. They are fixed values defined at compile-time.
Their assigned values must be explicitely given and must be expressions which the result can be determined at compile-time.

const $stuff = 5 declares a constant named $stuff with the number 5 as its value const $stuff = "hello" declares a constant named $stuff with the text "hello" as its value

Declaring a variable

Variables are named values that can change throughout the execution of a program.
We may or may not assign them an initial value.
If we do not assign an initial value, the default generic value is used, and we must provide a type.
A variable is only accessible within the scope it has been declared in. For instance, if we declare a variable at the biginning of a function, it is available throughout the entire function. If we declare a variable within a if block, it is only available within that block, until the corresponding else, elseif or until going back to the parent scope.
A variable declared in the global scope is accessible from everywhere.
For variables declared in the global scope, when we assign it an initial value, the given expression must be determined at compile-time.
Variable names must start with a letter or underscore (a-z or _) then must contain only alphanumeric characters and underscores.

var $stuff = 5 declares a number variable with an initial value set to 5 when the program starts
var $stuff = "hello" declares a text variable with an initial value set to "hello" when the program starts
var $stuff:number declares a number variable with an initial value set to 0 when the program starts
var $stuff:text declares a text variable with an initial value set to "" when the program starts
var $stuff = position() declares an instance of an implementation-defined object of the type position (this is an example)

Implementation-defined objects cannot be declared without initialization, since they do not have a default value.

Assigning a new value to a variable

To assign a new value to a variable, we can simply start a statement with the variable name followed by a = and an expression the result of which will be the new value.
We may also use a trailing function which will inherently modify the value of said variable.

$stuff = 5 assign the value 5 to the variable named $stuff
$stuff = $other + 5 assign the result of the expression ($other + 5) to the variable named $stuff
$stuff.round() call a trailing function that rounds the value of the variable

Declaring an array

An array is a dynamic list of values of a certain type. We can append or erase values, we can access a specific value from the list, or loop through all its values.
When declaring an array, we cannot specify an initial value, and we must provide a type.
Arrays are initialized with zero size when the program starts, values may be added/erased/modified throughout the execution of the program

array $stuff:number declare an array of numbers
array $stuff:text declare an array of texts

Arrays cannot contain implementation-defined objects, just generic types.

Declaring storage

Storage is used to keep some data persistent across power cycles and even through a re-compile.
We can declare storage variables and arrays of either number or text.
Storage should ONLY be declared in the global scope.

storage var $stuff:number
storage var $stuff:text
storage array $stuff:number
storage array $stuff:text

Accessing/Assigning the nth item within an array

To access or modify the value of a specific item in an array, we must use the trail operator . followed by the 0-based index of the item or a variable containing that index
$stuff.0 = 5 Assign the value 5 to the first item of the array
$stuff.$index = 5 Assign the value 5 to the item with an index defined by the value of $index
$value = $stuff.1 Assign the value of the second item of the array to the variable $value

Accessing/Assigning the nth character within a text variable

Text variables work in a very similar way to arrays. We can use the trail operator . to access or modify the value of specific charaters within a text.
$myText.0 = "a" Set "a" as the first character of $myText

Key-Value objects

XenonCode supports its own key-value type that is always stored as text.
Simply declare a text variable and assign/read its members using its key as the trailing member

var $myObject = ".a{5}.b{8}" ; you can use the serialization format like so, but you don't have to, you may simply start with an empty text and assign the members one by one
print($myObject.a) ; will print 5
$myObject.b += 2 ; adds 2 to b which was 8 and will now be 10

The Init function

The Init function's body will be executed first everytime the virtual computer is powered on.
The init function cannot be called by the user. It can only be defined, and the device will automatically call it upon virtual startup.

init
    $stuff = 5
    @func1()
    ;...

Tick function

The tick function is executed at the beginning of every clock cycle of this virtual computer.
The tick function cannot be called by the user. It can only be defined, and the device will automatically call it for each cycle.

tick
    ; This body is executed once per clock cycle at the virtual computer's frequency

Timer functions

Timer functions are executed at a specified interval or frequency, but at most once per clock cycle.
We can either specify an interval as in every N seconds or a frequency as in N times per second.
Timer functions cannot be called by the user. They can only be defined, and the device will automatically call them at their appropriate time.

timer frequency 4
    ; stuff here runs 4 times per second
timer interval 2
    ; stuff here runs once every 2 seconds

Note: If the clock speed of the virtual computer is slower than the given interval or frequency, that timer function will not correctly run at the specified interval or frequency, and may be executed at every clock cycle instead.

Input functions

Input functions are a way of accessing the information that we have received from another device.
They may be executed any number of times per clock cycle, depending on how much data it has received since the previous clock cycle. The implementation may decide to only run it once per cycle using only the latest data received.
Devices may have an upper limit in the receiving buffer which defines the maximum number of times the input function may be called per clock cycle.
If that limit has been reached, only the last N values will be kept in the buffer.
Input functions are like user-defined functions, containing arguments, but no return value, and also we must specify a port index.
The port index must be specified after the input keyword and a trail operator .
The port index may be specified via a constant as well (must be known at compile time).
Function arguments must be surrounded with parenthesis and their types must be specified.
Input functions cannot be called directly by the user. They can only be defined, then the device will automatically call them if data has been received, at the end of a clock cycle.

input.0 ($arg1:number, $arg2:text)
    $stuff = $arg1
input.$myPortIndex ($arg1:number, $arg2:text)
    $stuff = $arg1

Output

The output function is how we send data to another device. This function is meant to be called as a statement, and cannot be used in the global scope like the input functions are.
We must also pass in the port index as we do with the input function, and it can also be specified via a constant that is known at compile-time.
We must pass a list of arguments surrounded with parenthesis (or an empty set of parenthesis).
output.0 ($stuff, $moreStuff)

If Elseif Else

Like most programming languages, we can use conditionals.

if $stuff == 5
    ; then run this
elseif $stuff == 6
    ; then run that instead
else
    ; all previous conditions evaluate to false, then run this instead

Foreach loops

This loops through all items of an array.
The block of code under that loop statement will be executed for every item in the array, one by one.

foreach $stuff ($index, $item)
    ; we loop through the array $stuff, and we define $index which contains the 0-based index of this item and $item for the current item's value
    ; note that $item is a copy of its value, so modifying the value of $item will not affect the original array $stuff
    ; if we want to persist the modified $item value into the original array, we can use $index to index the element from the array like so:
    $stuff.$index = $item
    ; CAUTION: $index is a reference used internally for the loop, don't modify its value unless you actually want to affect the loop

You may also use the foreach loop with key-value objects

foreach $obj ($key, $value)
    print($key)
    print($value)

Repeat loops

This loop will repeat the execution of the following block a given number of times.

repeat 5 ($i)
    ; this block will be repeated 5 times, and $i is the 0-based index of this iteration (first time will be 0, last will be 4)
    ; CAUTION: $i is a reference used internally for the loop, don't modify its value unless you actually want to affect the loop

The number of times (above specified as 5) may also be specified via a variable or a constant, but not an expression

While loops

This loop will run the following block as long as the given condition evaluates to true.

while $stuff < 5
    $stuff++

Break

This keyword is used to stop a loop as if it completed all its iterations.

while $stuff < 5
    $stuff++
    if $stuff == 3
        break

Continue

This keyword is used to stop this iteration of a loop here and run the next iteration immediately.

while $stuff < 5
    $stuff++
    if $stuff == 2
        continue

Math Operators

Trailing Operators

Assignment operators

The following operators will compute the appropriate math operation between the leading variable and the following expression, then assign the result back to the leading variable.

Conditional Operators

Other operators

Casting (parse variables as another type)

To parse an existing variable as another type, simply add a colon and the type, like so:

$someTextValue = $someNumberValue:text

This only works with generic types number and text, not arrays or objects.

String concatenation

To concatenate texts, simply separate all text values/variables with the concat operator & in the same assignment (don't forget to cast to text if you need to).

$someTextVar = "Hello, " & $someName & "!, My age is " & $age:text & " and I have all my teeth"

Include

You may want to split your project into multiple source files.
To do this, you can put some code into another .xc file and use the include keyword in the parent file.

include "test.xc"

This is effectively the same as taking all the lines from test.xc and inserting them into the current file where the include is.
This can be done on multiple levels, just make sure that a file does not include itself, directly or indirectly, in which case the definitions within it will conflict and result in a compile error.

User-Defined Functions

Functions define a group of operations that we may call one or more times during the execution of the program.

The operations to run in a function appear within its body.

Functions may have arguments that can be used within the body so that the operations may have a variation depending on the value of some variables.

Function arguments are defined after the function name, within parenthesis and they can be of type number, text, or implementation-defined object.

Function names have the same rules as variable names.

NOTE: Functions MUST be fully defined before their use. This means that the order of function declaration matters and we can only call a function that has been declared ABOVE the caller and a function CANNOT call itself. This enforces the "no-stack-recursion" rule.

Declaration

Here are some function declaration examples

This function takes in a single number argument:

function @func0 ($var1:number)

This function takes two number arguments:

function @func1 ($var1:number, $var2:number)

This function takes a number argument and a text argument:

function @func2 ($var1:number, $var2:text)

This function takes an implementation-defined object type position argument:

function @func3 ($var1:position)

This function takes a number argument and a text argument and returns a number value:

function @func2 ($var1:number, $var2:text) : number

Body

The body of a function (the operations to be executed when calling it) must be on the following lines after the declaration, indented with one tab.
Empty lines within a body are allowed and ignored by the compiler.
Functions bodies may have a return statement optionally followed by an expression that would be used to assign a leading variable in the caller statement.
When returning a value, the return type must be provided at the end of the arguments, after the closing parenthesis and a colon.

function @func1 ($var1:number, $var2:number) : number
    return $var1 + $var2

Call

Calling a function will run the operation in its body. To call a function, simply write the name of the function (starting with @ for user-defined functions), then its arguments within parenthesis separated by commas, like so:

@func1(4, 6)

Here we have passed two number arguments, thus this call executes the body of the declaration above.
It is of course also possible to use variables or even complex expressions instead of just literal numbers as the function arguments.

NOTE:

Omitted arguments are legal.
Their value initially takes the default empty ("" or 0) then persists with what is passed in or assigned to them for the following calls to that function.
Changing the value of an argument within that function will also be persistent for the next call to that function, if that argument is omitted. Hence, if a concept of a default argument value is needed, they can be assigned to the argument in the body of that function after their use.
This omitted argument concept can also be thought of as a concept similar to static local variables in C++.

Return value

Functions may return a value using the return keyword.
This returned value may be assigned to a variable like so :

$value = @func1(4, 6)

Trailing functions

Any function may be called as a trailing function, even user-defined functions.
The way they work is that under the hood the leading variable is passed as the first argument to that function, and then assigned the returning value.
When calling a trailing function, we must ommit the first argument as it automatically sends the leading variable as its first argument under the hood.
If the function definition does not have any arguments, this is still valid, although we simply don't care about the current value of the leading variable, but we'll assign a new value to it.
The function definition MUST have a return type that matches that of the leading variable, if it's a generic type.
A trailing function may be called on implementation-defined objects, in which case the first argument must be of that object type, there is no return type in the function and it must NOT return any value.
Since we cannot pass Arrays as function arguments, arrays can only take their own specifically defined trailing functions.

$myVariable.round()
$myVariable.@func1(6)
$myArray.clear()

Built-in functions

Math

These functions are defined in the base language and they take one or more arguments.
Trailing math functions will use the leading variable as its first argument and modify that variable by assigning it the return value.

Text functions

Formatting

The text function takes a format as the first argument.
The format is basically a text that may contain enclosing braces that will be replaced by the value of some variables or expressions.
Exemple:

$formattedText = text("My name is {} and I am {} years old.", $name, $age)

Empty braces above will be replaced by the corresponding variables in the following arguments in the same order.
It is also possible to format number variables in a specific way by providing some pseudo-values within the braces like so:

Trailing functions for Arrays

These functions MUST be called as trailing functions, and they do not return anything, instead they modify the array

Trailing members for Arrays

Using the trail operator ., we can also return a specific information about certain types of variables.

Other useful helpers

Device functions

An implementation should define application-specific device functions.
Here are examples of basic device functions that MAY or MAY NOT be defined:

Compiler Specifications

This section is intented for game developers who want to use this in their game.

XenonCode comes with its own parser/compiler/interpreter library, and a very simple cli tool.

Editor

It is encouraged that the code editor runs the following parse on the current line on each keystroke:

Runtime

Upon powering up the virtual computer:

One clock cycle, executed 'frequency' times per second:

Testing XenonCode

You may want to test XenonCode or practice your coding skills.
There is an online fiddle tool at XenonCode.com

Otherwise, you may want to try running it directly on your computer.
For this, XenonCode has a cli with a -run command to test some scripts in the console.
This repository comes with the cli tool, located in build/xenoncode
Here's how you can download and run XenonCode:

# Clone this github repository
git clone https://github.com/batcholi/XenonCode.git
cd XenonCode
# Compile & Run the XC program located in test/
build/xenoncode -compile test -run test

You may edit the .xc source files in test/ then try running the last line again to compile & run.
test/storage/ directory will be created, it will contain the storage data (variables prefixed with the storage keyword).
Note that this -run command is meant to quickly test the language and will only run the init function.
Also, make sure that your editor is configured to use tabs and not spaces, for correct parsing of indentation.

If you want to integrate XenonCode into your C++ project, you can include XenonCode.hpp.
Further documentation will be coming soon for this, in the meantime you may use main.cpp as an example but its usage is still subject to change.