Function Namespaces

Each Function is a Separate Compilation Unit

Functions in Rhai are pure and they form individual compilation units.

This means that individual functions can be separated, exported, re-grouped, imported, and generally mix-’n-matched with other completely unrelated scripts.

For example, the AST::merge and AST::combine methods (or the equivalent + and += operators) allow combining all functions in one AST into another, forming a new, unified, group of functions.

Namespace Types

In general, there are two main types of namespaces where functions are looked up:

NamespaceQuantitySourceLookupSub-modules?Variables?
Globalone
  1. AST being evaluated
  2. Engine::register_XXX API
  3. global registered modules
  4. functions in imported modules marked global
  5. functions in registered static modules marked global
simple nameignoredignored
Modulemany
  1. Module registered via Engine::register_static_module
  2. Module loaded via import statement
namespace-qualified nameyesyes

Module Namespaces

There can be multiple module namespaces at any time during a script evaluation, usually loaded via the import statement.

Static module namespaces can also be registered into an Engine via Engine::register_static_module.

Functions and variables in module namespaces are isolated and encapsulated within their own environments.

They must be called or accessed in a namespace-qualified manner.

import "my_module" as m;        // new module namespace 'm' created via 'import'

let x = m::calc_result();       // namespace-qualified function call

let y = m::MY_NUMBER;           // namespace-qualified variable/constant access

let z = calc_result();          // <- error: function 'calc_result' not found
                                //    in global namespace!

Global Namespace

There is one global namespace for every Engine, which includes (in the following search order):

  • all functions defined in the AST currently being evaluated,

  • all native Rust functions and iterators registered via the Engine::register_XXX API,

  • all functions and iterators defined in global modules that are registered into the Engine via register_global_module,

  • functions defined in modules registered into the Engine via register_static_module that are specifically marked for exposure to the global namespace (e.g. via the #[rhai(global)] attribute in a plugin module).

  • functions defined in imported modules that are specifically marked for exposure to the global namespace (e.g. via the #[rhai(global)] attribute in a plugin module).

Anywhere in a Rhai script, when a function call is made, the function is searched within the global namespace, in the above search order.

Therefore, function calls in Rhai are late bound – meaning that the function called cannot be determined or guaranteed; there is no way to lock down the function being called. This aspect is very similar to JavaScript before ES6 modules.

// Compile a script into AST
let ast1 = engine.compile(
r#"
    fn get_message() {
        "Hello!"                // greeting message
    }

    fn say_hello() {
        print(get_message());   // prints message
    }

    say_hello();
"#)?;

// Compile another script with an overriding function
let ast2 = engine.compile(r#"fn get_message() { "Boo!" }"#)?;

// Combine the two AST's
ast1 += ast2;                   // 'message' will be overwritten

engine.run_ast(&ast1)?;         // prints 'Boo!'

Therefore, care must be taken when cross-calling functions to make sure that the correct function is called.

The only practical way to ensure that a function is a correct one is to use modules – i.e. define the function in a separate module and then import it:

┌──────────────┐
│ message.rhai │
└──────────────┘

fn get_message() { "Hello!" }


┌─────────────┐
│ script.rhai │
└─────────────┘

import "message" as msg;

fn say_hello() {
    print(msg::get_message());
}
say_hello();