Skip to content

Getting Started

Writ is an embedded scripting language for Rust. Add it as a dependency, write a .writ script, and load it from your application.

[dependencies]
writ = "0.1"

Create scripts/hello.writ:

func greet(name: string) -> string {
return "Hello, " + name + "!"
}
print(greet("World"))

Every Writ program runs inside a Writ instance. Create one — the standard library is loaded automatically:

use writ::Writ;
let mut vm = Writ::new();

load() compiles the file, executes top-level code, and makes functions available for call():

vm.load("scripts/hello.writ").unwrap();
// Call a script function from Rust.
let result = vm.call("greet", &[writ::Value::Str("Rust".into())]).unwrap();
println!("{result}"); // Hello, Rust!

Register Rust functions so scripts can call back into your code:

use writ::{Type, fn2};
vm.register_host_fn(
"damage",
vec![Type::Float, Type::Float],
Type::Float,
fn2(|hp: f64, amount: f64| -> Result<f64, String> {
Ok((hp - amount).max(0.0))
}),
);

Now scripts can call damage() as if it were a built-in:

let remaining = damage(100.0, 35.0)
print(remaining) // 65.0

The type checker validates calls at compile time — wrong argument types or counts produce errors before the script runs.

Here’s a more complete example — a script with a class, and Rust code that loads and drives it:

scripts/combat.writ
class Fighter {
public name: string
public health: float = 100.0
public func takeDamage(amount: float) {
health -= amount
if health <= 0 {
print(name + " was defeated!")
}
}
public func isAlive() -> bool {
return health > 0
}
}
func createFighter(name: string) -> Fighter {
return Fighter(name: name)
}
use writ::{Value, Writ};
fn main() {
let mut vm = Writ::new();
vm.load("scripts/combat.writ").unwrap();
let fighter = vm.call("createFighter", &[Value::Str("Hero".into())]).unwrap();
println!("{fighter}"); // Fighter { name: "Hero", health: 100.0 }
}