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
orfalse
value (yes
andno
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 |
|
|
*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 |
|
Given the custom function named add is constant:
|
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
Operator | Operation | Example |
---|---|---|
- | Negates a value | ---5 = -5 |
! | Inverts a value | !false = true |
Arithmetic Operators
Operator | Operation | Example |
---|---|---|
+ | Adds two values | 1+2 = 3 |
- | Substracts two values | 3-2 = 1 |
* | Multipies two values | 2*3 = 6 |
/ | Divides two values | 4/2 = 2 |
% | Remainder of a division | 5%3 = 2 |
Logic / Bitwise Operators
Operator | Operation | Example |
---|---|---|
&& | AND Logic gate | true && false = false |
\|\| | OR Logic gate | true \|\| false = true |
^ | XOR Logic gate | true ^ true = false |
<< | Shift n times to the left | 1<<2 = 5 |
>> | Shift n times to the right | 5>>2 = 1 |
== | Compares if two values are equal | 5==5.0 = true |
!= | Compares if two values are different | 5!=5 = false |
>= | Compares if first value is greater or equal than the second | 5>=6 = false |
<= | Compares if first value is lower or equal than the second | 5<=6 = true |
> | Compares if first value is greater than the second | 5>5 = false |
< | Compares if first value is lower or equal than the second | 5<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 |
|
|
|
|
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 n isn't known when compiling this script:
- if n is known when compiling this script and n>5:
- if n is known when compiling this script and 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 |
|
|
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 beInto<MoonValue>
andTryFrom<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 aSimpleError
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 afn(...) -> 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>
orInputVariable::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:
- 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.
- Lazy variables: These will get calculated once every script run, unlike
constants or inlined variables, which are already calculated.
- 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.
- 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
orOptimizedASTExecutor::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
.