Moon Script

This is a very basic scripting language for simple scripting with some syntax based on Rust's.

It doesn't mean to compete with other scripting languages implemented in Rust like Rhai or dyon, and therefore lacks many features these have, for example, Moon Script doesn't allow creating custom types yet, like Rhai does, but allow to create 'associations', which are values that can be represented through a series of what MoonScript consider primitives, like integers, strings, or booleans, for example, an human ID can be expressed as the primitive 'array' with values [name: String, age: integer].

The goal of MoonScript is for those using it to find themselves scripts in the simplest manner possible, while still boosting performance, examples of these simplified manners could be not needing to write semi-colons (;) at the end of lines, or not writting commas (,) to separate elements, for example, this code are actually two valid sentences in MoonScript:

variable=function(1 2 3)
other_variable="No commas or semicolons used"

Values

In Moon Script every value is a primitive between an integer (i128), decimal (f64), boolean (bool), text (String), null, or arrays, where each array may contain different types of said primitives or even other arrays, like for example, a Moon Value can be:

[0, false, null, empty, "Text",
  ["This array is inside another array",
    [0, "This array is in an array inside in another array"]
  ]
]

Separating values with commas is highly recommended, but they don't need to be, for example, making the following a valid array with numbers 1, 2 and 3:
[1 2 3]

This lists every single type of Moon value:

  • null = A null value (empty is also valid)
  • boolean = A true or false value (yes and no is also valid)
  • decimal = Numbers with decimal places, like 0.1, -1.2, or .1
  • integer = Numbers with no decimal places, like 1, 5, 132, -4
  • string: Any text between two ", like "My text"
  • array: A collection of Moon values enclosed by '['' ']', like: [1, true, "Hi", null]
    Arrays' values can be accessed by index following them by [*index*], like:
let my_array = ["First", "Second", "Third"]
print(my_array[1]) //Prints "Second"

Assignments

Values might be assigned to a variable, where variables' names must start with a letter and can be followed by more letters, numbers, :, and _.

In Rust it is common to set a variable preceded by in let, however, that's not required in here, meaning both of these statements are correct:

let my_number = 5
my_number = 5

A value might be reassigned in the same fashion as in an assignment:

my_number = 5
my_number = 10

Using let allows you to just clarify ambiguity when working with nested blocks:

modify_me = "I am unmodified"
do_not_modify_me = "I am unmodified"
if true {
    modify_me = "Modified!" //This reassigns 'modify_me'
    let do_not_modify_me = "Assigned!" //This create a new variable 
                                       // only available on this scope
    print(modify_me) //Prints Modified!
    print(do_not_modify_me) //Prints Assigned!
}
print(modify_me) //Prints Modified!
print(do_not_modify_me) //Prints I am unmodified

Input variables

If you are not the developer of the program that is using MoonScript, you might find the developers used Input Variables, these are variables the developers include into your code without you requiring to use it.

For example, if the context of the program was a car rental, you might be writing a script for actions to take after an user has asked to rent a specific car, to allow you using them, the developer might give you variables like car, appointment, or client so you can access a bigger context, when they do, you don't need to do anything special, you can use them directly, like this:

println("Sending email to the client with rent details");
client.send_email("You have rented a car for the date "+appointment.date.to_string);

For more information about the Input Variables used by your program, contact its developers and their documentation.

Optimizations

Values and variables known at compile-time* might get inlined when possible:

Input Moon Script Optimization
a = 5
b = a
print(b)
print(5)

*This includes some of the Input Variables

Returning values

Like in Rust, you can return a value either by writing a return statement:

let a = 5
return a //Returns 5
return 5 //Returns 5

... Or, if the last statement yields a value, it also returns it like in Rust:

let a = 5
let b = 0
a //Returns 5
a = 10
5 //Returns 5

Calling functions

Functions might be called directly by their name, like:

print("Hi")

When calling a function with multiple parameters, it isn't needed for you to separate these arguments with commas, making the following calls valid:

my_function(1, null, "With commas")
my_function(1 null "No commas!")

If you are using an implementation with multiple modules, two modules can have the same function for a name, like 'sum' function in both a 'math' and an 'my_nums' module, to disambiguate from them, you can precede the module's name to the function, like:

math/sum("1 2 3")    //Calls 'sum' from the 'math'    module
my_nums/sum("1 2 3") //Calls 'sum' from the 'my_nums' module


*Note that Moon Script comes with very few functions, currently just having 'print' and 'println' functions to print one value.

Optimizations

Some functions are constant, meaning that when its arguments are known at compile time, their result will be calculated at compile time to prevent needlessly calculating the same value over each execution:

Input Moon Script Optimization
a = 5
b = 10
sum = add(a b)
return sum

Given the custom function named add is constant:

return 15

Properties

Some functions have the first parameter as a property, meaning you can call them directly from the object in a more concise manner:

let x = point.x //Assigns the value of x from Point to a variable
let x = get_x(point) //This executes the same as the previous statement,
                     //but it's more verbose

Some properties can also receive arguments:

let x = point.set_x_and_take(5) //Saves the value of x from Point
let x = set_x_and_take(point 5) //This executes the same as the previous statement,
                                //but feels harder to read

Getters and Setters as Properties

Calling a property with no parameters, it's the same as calling either a function named get_*name* or *name*, like:

let x = point.x //Assigns the value of x from Point to a variable
let x = get_x(point) //This executes the same as the previous statement,
                     //but it's more verbose

However, if the property is an assignment, it won't search for get_*name*, but set_*name* instead

point.x = 5 //Assigns the value of x from Point to a variable
set_x(point 5) //This executes the same as the previous statement,
               //but it's more verbose

Optimizations

Properties are essentially just functions, meaning if their function and their arguments are both constant, they can get inlined in the same fashion as shown in the functions section.

Operators

Unary Operators

OperatorOperationExample
-Negates a value---5 = -5
!Inverts a value!false = true

Arithmetic Operators

OperatorOperationExample
+Adds two values1+2 = 3
-Substracts two values3-2 = 1
*Multipies two values2*3 = 6
/Divides two values4/2 = 2
%Remainder of a division5%3 = 2

Logic / Bitwise Operators

OperatorOperationExample
&&AND Logic gatetrue && false = false
\|\|OR Logic gatetrue \|\| false = true
^XOR Logic gatetrue ^ true = false
<<Shift n times to the left1<<2 = 5
>>Shift n times to the right5>>2 = 1
==Compares if two values are equal5==5.0 = true
!=Compares if two values are different5!=5 = false
>=Compares if first value is greater or equal than the second5>=6 = false
<=Compares if first value is lower or equal than the second5<=6 = true
>Compares if first value is greater than the second5>5 = false
<Compares if first value is lower or equal than the second5<5 = false

Optimizations

All operators are essentially functions, and they are all also constant, that when the values they entail are constant (known at compile-time), their results will be inlined as it happens with functions.

Input Moon Script Optimization
return 10 + 5
return 15
let num = 15
let comparasion = num > 10
return comparasion
return true

If blocks

The following syntax allows you to create a series of conditions followed by some code to execute, the first condition met will be the one to trigger it's matching block:

if condition_1{
    statements_1
} else if condition_2 {
    statements_2
} else if condition_3 {
    statements_3
...
} else {
    statements_n
}

for example:

if number >= 10{
    println("This number has at least two digits")
} else {
    println("This number is either negative or has less than two digits")
}

The else statements are not mandatory, making the following also valid:

if number >= 10{
    println("This number has at least two digits")
}

Optimizations

When the conditions for an if block are constant, they might be inlined to lower the performance and memory costs of both executing and storing lines that will never be visited, the following examples will be transformed:

Input Moon Script Optimization
if true{
    println("Always executed")
} else{
    println("Never executed")
}
println("Always executed")
if true {
    println("Always executed")
} else if n>5{
    println("Never executed")
} else{
    println("Never executed")
}
println("Always executed")
if n>5 {
    println("Only executed when n>5")
} else if true{
    println("Only executed when not n>5")
} else{
    println("Never executed")
}
- if n isn't known when compiling this script:
if n>5 {
    println("Only executed when n>5")
} else if true{
    println("Only executed when not n>5")
}

- if n is known when compiling this script and n>5:

println("Only executed when n>5")

- if n is known when compiling this script and not n>5:

println("Only executed when not n>5")

While blocks

The following syntax allows you to create a loop happening every time it's condition is met:

while condition {
    statements
}

Note that break is NOT currently supported nor planned, although return IS supported.

Optimizations

When the condition of a while block is known at compile-time and it is false, said block is removed as it will never execute, for example:

Input Moon Script Optimization
print("Before loop")
while false{
    print("Never happening")
}
print("After loop")
print("Before loop")
print("After loop")

Engine

The Engine allows you to compile ASTs out of scripts source code, and these ASTs can be run directly:

let engine = Engine::new();
let context = ContextBuilder::new();

// Create an AST out of a script that prints to the standard output
let ast = engine.parse(r###"println("Hello world")"###, context).unwrap();

/// Execute the AST
ast.execute();

The Engine type allows you to configure how these scripts are compiled, allowing you to indicate constants, functions or types that are common to all your scripts, this will be specified in the following pages.

Adding constants

Constants are values that are known when compiling your scripts, this allows to inline them to execute the multiple optimizations that you can find in the user's guide.

To create a constant you just need to call the Engine::add_constant function with a name for your constant and a value, said value must implement Into <MoonValue> (Or implementing From<T> for MoonValue), this is already implemented for rust most basic types: bool, String, (), i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32 and f64.

let mut engine = Engine::new();

// Create a constant named ONE
engine.add_constant("ONE", 1);

// Creates and executes a script that returns the constant
let ast_result = engine.parse(r###"return ONE;"###, ContextBuilder::new()).unwrap()
    .execute().unwrap();

assert_eq!(MoonValue::Integer(1), ast_result);

// The value returned by an AST execution is a MoonValue, luckily, MoonValue implements
// TryFrom for basic rust primitives and String, so we can get it with try_into()
// as an i32
let ast_result_as_i32 : i32 = ast_result.try_into().unwrap();
assert_eq!(1, ast_result_as_i32);

Adding functions

Custom-made functions can be added to the Engine as long as its parameters and the return type implement Into<MoonValue> and TryFrom<T> for MoonValue, this is already implemented for Rust basic types like String or i32, a MoonValue itself, or a custom-made type you made as explained in the Custom Types section.

The following example creates and uses a function named sum_two for it to be called and executed inside a MoonScript:

let mut engine = Engine::new();

// Creates a function that adds two numbers, this function is a function that can be
// called at compile time, so we also call 'inline' to enable this optimization.
let function_sum_two = FunctionDefinition::new("sum_two", |n: u8, m: u8| n + m)
    .inline();

// The function is added to the engine
engine.add_function(function_sum_two);

// Creates and executes a script that sums 1 and 2 and returns its result
let ast_result : i32 = engine.parse(r###"sum_two (1,2);"###, ContextBuilder::new())
    .unwrap().execute().unwrap().try_into().unwrap();
assert_eq!(3, ast_result);

Using Result<Value, Error>

The return type of the function can also be Result<DesiredValue, ErrorDescription>, where:

  • DesiredValue: It's the value the function will return to the Script, this still requires for it to be Into<MoonValue> and
  • TryFrom<MoonValue> for DesiredValue.
  • ErrorDescription is either a &str or a String describing why the error happened, if the execution finds this value, it will return it as a SimpleError description, this way your script developer can find what's wrong.

In this example, the function sum_two now checks for the two values to not overflow u8's range, returning a line telling the two numbers for cases where they are larger:

let mut engine = Engine::new();

// Creates a function that adds two numbers, this function is a function that can be
// called at compile time, so we also call 'inline' to enable this optimization.
let function_sum_two = FunctionDefinition::new("sum_two", |n: u8, m: u8| {
        n.checked_add(m).ok_or(format! ("Error, numbers too large ({n}, {m})"))
    })
    .inline();

// The function is added to the engine
engine.add_function(function_sum_two);

// Creates and executes a script that sums 1 and 2 and returns its result, not failing
let ast_result: i32 = engine.parse(r###"sum_two(1,2);"###, ContextBuilder::new())
.unwrap().execute().unwrap().try_into().unwrap();
assert_eq!(3, ast_result);

// Creates and executes a script that sums 100 and 200, forcing the compilation to fail
let compilation_error = format!("{}",
    engine.parse(r###"sum_two(100,200);"###, ContextBuilder::new())
    .err().unwrap());
println!("{}", compilation_error);

The previous code will show a compilation error, printing the following to the standard output:

Error: Could not compile. Cause:

  • Position: On line 1 and column 1
  • At: sum_two(200,200)
  • Error: The constant function sum_two was tried to be inlined, but it returned this error:
                Could not execute a function due to: Error, numbers too large (100, 200).

Creating custom types

MoonScript doesn't use the real concept of a custom type, but rather an association, this means every custom type will be turned into a MoonValue during the script's executing, and will return a MoonValue, for this matter, your custom types must implement From<MyType> for MoonValue and TryFrom<MoonValue> for MyType, which is planned to be a derive macro in the future.

The following example shows how you can create a custom type:

struct MyType {
    name: String,
    age: u16,
}

impl From<MyType> for MoonValue{
    fn from(value: MyType) -> Self {
        MoonValue::from(vec![
            MoonValue::from(value.name),
            MoonValue::from(value.age)
        ])
    }
}

impl TryFrom<MoonValue> for MyType {
    type Error = ();

    fn try_from(value: MoonValue) -> Result<Self, Self::Error> {
        match value {
            MoonValue::Array(mut moon_values) => Ok(
                Self{
                    name: String::try_from(moon_values.remove(0)).map_err(|_|())?,
                    age: u16::try_from(moon_values.remove(0)).map_err(|_|())?,
                }
            ),
            _=>Err(())
        }
    }
}

If these requirements are met, then it's type can be used when defining constants, functions parameters and return types, or input variables, for example, this way you can create a constant value of MyType and also create a getter function that gets a value of it:

let mut engine = Engine::new();

// Create a value for the type
let my_type_example = MyType { name: "Jorge".to_string(), age: 23 };

// Create a constant with said type
engine.add_constant("BASE_HUMAN", my_type_example.clone());

// Create a getter for the field age
engine.add_function(FunctionDefinition::new("age", |value:MyType|{
    value.age
})
    // The function is associated to the type 'MyType', this means the function age
    // will work as a property, so instead of calling 'age(BASE_HUMAN)',
    // we write 'BASE_HUMAN.age', for more information about properties, check the
    // properties secion of the user's guide
    .associated_type_of::<MyType>());

// Create and execute a script that uses the custom constant and function, where
// it gets the age of the human
let age : u16 = engine.parse("BASE_HUMAN.age", Default::default())
    .unwrap().execute().unwrap().try_into().unwrap();

assert_eq!(my_type_example.age, age);

Specific scripting contexts

A ContextBuilder allows you to give specific configuration for a script, instead of globally as the Engine does.

Input variables

Input Variables allows you to include variables on your script without the need of your users to create or declare them:

let engine = Engine::new();

// Create a context with an inlined named user_name whose value is 'Jorge'
let context = ContextBuilder::new()
    .with_variable(InputVariable::new("user_name").value("Jorge"));

// Creates and executes a script that returns the value of said variable
let user_name : String = engine.parse("return user_name;", context)
    .unwrap().execute().unwrap().try_into().unwrap();

assert_eq!("Jorge", user_name);

There are three kinds of Input Variables:

  • Inlined variables: These are given through the InputVariable::value method and gives an exact value, these variables will be inlined on the AST.

  • Lazy / Produced variables: These are given through the InputVariable::lazy_value method and gives a fn(...) -> Into<MoonValue>, these variables won't be directly inlined, but their value will be calculated just once every time the script is run.

  • Late variables: These require you to give them a type through either InputVariable::associated_type_of<T> or InputVariable::associated_type to allow the Engine to discover it's proper type and therefore functions, but in exchange, you don't need to give the value on the ContextBuilder, you can give them just before executing the script, meaning the value can change every time you run the script.

    This might seem similar to Lazy variables, but this one allows you to fully customize the value, while a closure might be harder to handle.

    This example uses a Late variable instead of an Inlined one:
let engine = Engine::new();

// Create a context with a late variable named user_name whose type is that of a String
let context = ContextBuilder::new()
    .with_variable(InputVariable::new("user_name").associated_type_of::<String>());

// Compiles a script that returns the value of said variable and creates an executor to
// execute said script, to give the value of this variable to the executor, the method
// push_variable is called with the name of the late variable and it's value
let ast = engine.parse("return user_name;", context)
    .unwrap();
let ast_executor = ast.executor()
    .push_variable("user_name", "Jorge");

// Executes the AST to return the value of said variable
let user_name: String = ast_executor
    .execute().unwrap().try_into().unwrap();

assert_eq!("Jorge", user_name);

Optimizations

If you give an Input Variable, but the AST doesn't use it, said variable will not be included in the AST to prevent it from occupying memory, and in the case, a performance cooldown, as these won't be necessary to calculate.

Performance

If performance is a top priority, you must consider how you give the values in your scripts, as a matter of choosing, choose them with this priority order:

  1. Engine's constants or ContextBuilder's Inlined variables: These variables are always inlined in place, meaning their access is much faster, although these should be small values.

  2. Lazy variables: These will get calculated once every script run, unlike constants or inlined variables, which are already calculated.

  3. Let the user create the variable: These work internally in almost the same way as Lazy variables, meaning they have the a very similar impact.

  4. Late variables: They won't get inlined as constants or inlined variables, and they require a search of a HashMap on every start of script when you call ASTExecutor::push_variable or OptimizedASTExecutor::push_variable, unlike Lazy variables or used created variables.

Better error formatting

ASTs

As shown in the previous sections, calling Engine::parse will return an AST, these ASTs allows you to run the script, and they are independent of the Engine and ContextBuilder they were created with as any function, constant, or input variables are inlined on the AST itself, this means you can even remove and free an Engine and the AST will still work.

To run an AST you can directly call AST::execute, or create an ASTExecuter with AST::executor, the executor allows you to further customize the AST's execution, for example, ASTExecutor::push_variable allows you to push Late Variables (Check the Input Variables section) and then to call ASTExecutor::execute.

Independently on how you call it, you'll be given a Result<MoonValue, RuntimeError>, if the execution got an error, you will get Err(RuntimeError) that can display as shown in the 'Better error formatting' section.

If you get Ok(MoonValue), it means the execution was successful, and said it's the return value of the script, which you can turn into any type that implements TryFrom<MoonValue> for T, which is a requirement for custom types as specified in the 'Creating custom types', this is already implemented for bool, String, (), i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32 and f64.