Memory Model

Data objects in scripts are always passed by reference to a heap memory allocation.

This includes primitive types as well. For instance, if the script code introduces a boolean value using the true keyword, the result of this expression is a reference to the heap where the boolean data object is allocated.

The script engine maintains a counter for the active references to script data, ensuring that the object's data allocation persists until the last reference is released.

let x = 500; // Variable `x` contains a reference to the numeric object "500".

x /= 10; // `x` is passed by reference.

dbg(x); // `x` is passed by reference too.

// The end of the `x` lifetime.

dbg("Done");

Function closures prolong data lifetimes:

let func;

{
    let x = 5;
    
    // The function captures the data referred to by `x`.
    func = fn() { return x; };
    
    // The lifetime of the variable `x` ends here,
    // but the data it refers to remains alive.
}

dbg(func()); // Prints "5".

// The lifetime of the function ends here, along with its closure
// to the numeric object "5".

Data Projections

Script code can create a projection of one referential data object to a subset of its memory.

For example, when script code refers to an array by index or range, the interpreter does not create a copy of the element(s). Instead, it returns a reference to the array slice.

let array = [10, 20, 30];

// The `array[1]` expression returns a reference to the second element
// of the array.
let second = array[1];

// Mutates an element of the original array.
second = 200;

// Prints "200".
dbg(array[1]);

If this behavior is not desirable, the script code can copy the referred data using the *foo cloning unary operator.

let array = [10, 20, 30];

let second_copy = *(array[1]);

second_copy = 200;

dbg(array[1]); // Prints "20".

Dereferencing

All data operations requested from scripts are eventually performed by the host system (Rust code).

When a script sums two numbers, the operation is executed by the Rust function that implements the a + b operator, taking both operands by value.

Passing by value means transferring the data allocation from the script memory to the host system (or implicitly cloning the data if there are multiple active script references to it).

In general, the host code can access script data immutably, mutably, or by taking the data by value (which may require immutable access for cloning).

The type of data access is usually indicated by the corresponding exported function signature. For example, the host function fn foo(a: &usize, b: &mut bool) accesses the first argument immutably and the second argument mutably.

Script code does not need to manually dereference provided arguments; data dereferencing is handled automatically by the script engine.

However, this implicit dereferencing must comply with Rust's general rules of exclusive access:

  • There can be as many simultaneous active immutable dereferences of the same data allocation as needed.
  • But if the data is dereferenced mutably, other simultaneous mutable or immutable dereferences are forbidden.

Data dereferencing is managed by the script engine, and failure to comply with these rules results in runtime errors.

Such errors are rare because, typically, data dereferencing is localized.