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
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
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
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
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.