Skip to content

Search

Back
· 5 min read

Balaur Script

Balaur Script is the engine's scripting language: typed .bs files with Rust's syntax, compiled to WebAssembly. Measured beside Rune, Lua, Luau and Rust.

Balaur Script is the engine's scripting language: .bs files with static types and Rust's syntax, compiled to WebAssembly. Desktop, Android and the web run the same module, and iOS runs it translated to C.

Two dot plots of time against Rust, for particles and fib(25): Rust, then Balaur Script as C, on wasmtime and on wasmi, beside LuaJIT, Luau, Lua 5.4 and Rune

The language

pub struct Spinner {
    pub speed: float = 2.0,
    pub clockwise: bool = true,
    node: Node,
    angle: float,
    direction: float,
}

impl Spinner {
    fn init() {
        self.direction = if self.clockwise { 1.0 } else { -1.0 };
    }

    fn update(dt: float) {
        if input::action_just_pressed("reverse") {
            self.direction = -self.direction;
        }
        self.angle += dt * self.speed * self.direction;
        self.node.transform.rotation_euler = vec3(0.0, self.angle, 0.0);
    }
}
  • A pub field with a default is a property the inspector shows.
  • A struct is a value kept in WebAssembly locals, so a Vec3 allocates nothing. A class is one object every holder shares.
  • T? is a value or nil, and any holds free-form data such as an HTTP reply.
  • Strings, lists and classes are reference counted. No collector runs.
  • await task::seconds(0.5) parks an async fn until the engine wakes it.

Beside Rune, Lua and Rust

Balaur Script Rune 0.14 Lua 5.4, Luau Rust
Types static dynamic dynamic; Luau adds optional annotations static
Syntax Rust's, without lifetimes or a borrow checker Rust's local, end, tables from 1 Rust
Runs as WebAssembly: a JIT, an interpreter, the browser's engine, or C bytecode on Rune's VM bytecode on Lua's VM; LuaJIT and Luau --codegen emit machine code machine code
Memory reference counts reference counts a garbage collector ownership
Closures, match, enums no yes closures yes
Async async fn, await f() async fn, f().await coroutines async fn, f().await
fn fib(n: int) -> int { if n < 2 { n } else { fib(n - 1) + fib(n - 2) } }
fn fib(n) { if n < 2 { n } else { fib(n - 1) + fib(n - 2) } }
local function fib(n)
  if n < 2 then return n end
  return fib(n - 1) + fib(n - 2)
end
fn fib(n: i64) -> i64 { if n < 2 { n } else { fib(n - 1) + fib(n - 2) } }

Speed

particles × Rust fib(25) × Rust
Rust 1.32 ms 1.0 0.23 ms 1.0
Balaur Script → C 1.50 ms 1.1 0.29 ms 1.3
LuaJIT 2.1 1.95 ms 1.5 0.43 ms 1.9
Balaur Script, wasmtime 1.98 ms 1.5 0.36 ms 1.5
Luau 0.667, --codegen 15.7 ms 11.9 2.02 ms 8.8
Balaur Script, wasmi 20.6 ms 15.6 2.50 ms 10.9
Lua 5.4 44.3 ms 33.6 4.46 ms 19.4
Luau 0.667 56.5 ms 42.9 4.21 ms 18.3
Rune 0.14 356 ms 271 15.8 ms 68.9

The particle step, in Balaur Script and in Rust:

for i in 0..n {
    let x = xs[i] + vxs[i] * 0.016;
    if x < 0.0 || x > 100.0 { vxs[i] = -vxs[i]; }
    xs[i] = x;
}
for i in 0..n {
    let x = xs[i] + vxs[i] * 0.016;
    if !(0.0..=100.0).contains(&x) { vxs[i] = -vxs[i]; }
    xs[i] = x;
}
  • Apple M1, best of 80 runs each, with every runtime's calls interleaved. All nine return the same sum, bit for bit.
  • Balaur Script → C is what an export ships where clang and lld are found: the module translated to C and built at -O2.
  • The host makes every NaN it reads out of a module the canonical one, so its bits match on every machine. No engine rewrites NaNs per operation, which would put wasmtime's particles at 2.3×.
  • The rest of wasmtime's gap to C is Cranelift, which optimises less than clang.
  • An engine call costs 54 ns as C, 92 on wasmtime and 120 on wasmi, against Rune's 141.

Where it runs

A .bs file compiles to one .wasm module, which an engine runs: wasmtime, the browser's or wasmi. At export the module is also translated to C and built into a native library, which a game runs a script from when it holds that module

  • The editor and an Android or desktop game without the library run scripts on wasmtime. A cached module loads in 0.84 ms.
  • A browser runs the module on the page's engine, at 1.1× Rust compiled to WebAssembly.
  • [export] native_scripts puts the library beside the executable, in an iOS app's frameworks, or in an APK.
  • A saved file reloads in 2.3 ms on wasmtime, keeping each field by name.

The scripting manual has the whole language.