Constants

Constants can be defined using the const keyword and are immutable.

const X;            // 'X' is a constant '()'

const X = 40 + 2;   // 'X' is a constant 42

print(X * 2);       // prints 84

X = 123;            // <- syntax error: constant modified

Tip: Naming

Constants follow the same naming rules as variables, but as a convention are often named with all-capital letters.

Manually Add Constant into Custom Scope

Tip: Singleton

A constant value holding a custom type essentially acts as a singleton.

It is possible to add a constant into a custom Scope via Scope::push_constant so it’ll be available to scripts running with that Scope.

use rhai::{Engine, Scope};

#[derive(Debug, Clone)]
struct TestStruct(i64);                                     // custom type

let mut engine = Engine::new();

engine
    .register_type_with_name::<TestStruct>("TestStruct")    // register custom type
    .register_get("value", |obj: &mut TestStruct| obj.0),   // property getter
    .register_fn("update_value",
        |obj: &mut TestStruct, value: i64| obj.0 = value    // mutating method
    );

let script =
"
    MY_NUMBER.update_value(42);
    print(MY_NUMBER.value);
";

let ast = engine.compile(script)?;

let mut scope = Scope::new();                               // create custom scope

scope.push_constant("MY_NUMBER", TestStruct(123_i64));      // add constant variable

// Beware: constant objects can still be modified via a method call!
engine.run_ast_with_scope(&mut scope, &ast)?;               // prints 42

// Running the script directly, as below, is less desirable because
// the constant 'MY_NUMBER' will be propagated and copied into each usage
// during the script optimization step
engine.run_with_scope(&mut scope, script)?;

Caveat – Constants Can be Modified via Rust

Tip: Plugin functions

In plugin functions, &mut parameters disallow constant values by default.

This is different from the Engine::register_XXX API.

However, if a plugin function is marked with #[export_fn(pure)] or #[rhai_fn(pure)], it is assumed pure (i.e. will not modify its arguments) and so constants are allowed.

A custom type stored as a constant cannot be modified via script, but can be modified via a registered Rust function that takes a first &mut parameter – because there is no way for Rhai to know whether the Rust function modifies its argument!

By default, native Rust functions with a first &mut parameter always allow constants to be passed to them. This is because using &mut can avoid unnecessary cloning of a custom type value, even though it is actually not modified – for example returning the size of a collection type.

In line with intuition, Rhai is smart enough to always pass a cloned copy of a constant as the first &mut argument if the function is called in normal function call style.

If it is called as a method, however, the Rust function will be able to modify the constant’s value.

Also, property setters and indexers are always assumed to mutate the first &mut parameter and so they always raise errors when passed constants by default.

// For the below, assume 'increment' is a Rust function with '&mut' first parameter

const X = 42;       // a constant

increment(X);       // call 'increment' in normal FUNCTION-CALL style
                    // since 'X' is constant, a COPY is passed instead

X == 42;            // value is 'X" is unchanged

X.increment();      // call 'increment' in METHOD-CALL style

X == 43;            // value of 'X' is changed!
                    // must use 'Dynamic::is_read_only' to check if parameter is constant

fn double() {
    this *= 2;      // function doubles 'this'
}

let y = 1;          // 'y' is not constant and mutable

y.double();         // double it...

y == 2;             // value of 'y' is changed as expected

X.double();         // since 'X' is constant, a COPY is passed to 'this'

X == 43;            // value of 'X' is unchanged by script

Implications on script optimization

Rhai assumes that constants are never changed, even via Rust functions.

This is important to keep in mind because the script optimizer by default does constant propagation as a operation.

If a constant is eventually modified by a Rust function, the optimizer will not see the updated value and will propagate the original initialization value instead.

Dynamic::is_read_only can be used to detect whether a Dynamic value is constant or not within a Rust function.