Foreword

"Why am I making a programming language?"

This question keeps rolling around in my head as I'm typing out the code that is slowly shaping into Rune. Programming is like magic. You imagine it in your mind, write it out, and there it is. Doing stuff which wasn't being done before.

Truth be told, I'm scared that people will tell me that I'm wasting my time. This has already been done, or "Why not just use X?". A thing so glaringly obvious that all of my efforts are wasted.

But you actually don't need a reason. It can simply be for The Joy of Creating, and then it's just you. Spending your own time. No harm done.

But I want to talk about why I'm making Rune beyond just for fun. So I'm dedicating this foreword to it. I feel obligated to describe why this might matter to others.

So here's why I'm making a new programming language.

I've spent a lot of effort working on OxidizeBot, a Twitch bot that streamers can use to add commands and other interactive things in their chat. I built it for myself while streaming. When adding features I always spend way too much time tinkering with it. Making it as generic as possible so it can solve more than just one problem. When it's a personal project, I don't care about being efficient. I care much more about doing things the right way.

...

Ok, I sometimes do that professionally as well. But a working environment is much more constrained. Personal projects should be fun!

Anyway, that means the bot isn't overly specialized to only suit my needs and can be used by others. It's starting to see a little bit of that use now which is a lot of fun. I made something which helps people do something cool.

All the commands in the bot are written in Rust, and compiled straight into the bot. This is nice because Rust is an incredible language. But Rust is also complex. Not needlessly mind you. I believe it's complex because it tackles really hard problems. And that usually comes with a base level of complexity it's very hard to get rid of.

But it's still tricky enough that streamers who have limited programming experience struggle getting up and running. I wanted them to be able to write their own commands. Ones they could just drop into a folder and presto - you're up and running.

To this day I've tutored two of these streamers who were interested in learning Rust to write their own commands.

Embedding a Rust compiler isn't feasible. So I started looking into dynamic programming languages. Ones that could be embedded into an existing application with little to no effort. That seamlessly integrates with its environment. A number of candidates came up, but the one that stood out the most to me was Rhai.

So why is Rhai awesome? It has Rust-like syntax. The runtime is fully written in mostly safe Rust, and can be easily embedded. Hooking up Rust functions is a piece of cake.

But Rhai has a set of design decisions which didn't exactly scratch my itch. The more I used it, the more I got inspired and started thinking about things that could be changed or added. I contributed a bit to the project. And it started to dawn on me that Rhai's approach wasn't exactly what I wanted. There's nothing wrong with this. The authors of Rhai have specific goals and ideas of what they want to accomplish. While it would be feasible to push Rhai in a different direction, the project would emerge looking much different on the other side. Which wouldn't be fair towards the people leveraging Rhai's strengths today. So I wanted a clean slate to find my own compromises. To discover freely what works and doesn't work well.

When I started working on Rune I had the following rough goals in mind:

  • Performance should be comparable to Lua and Python (And eventually LuaJIT when we have cranelift).
  • Scripts should compile quickly.
  • Rune should feel like "Rust without types".
  • Excellent support for asynchronous programming (i.e. native select statements).
  • Be as good as Rhai when it comes to integrating with native Rust.
  • Work well through C bindings.
  • A minimalistic stack-based runtime that is strictly single threaded*.

*: If this feels like a step backwards to you, don't worry too much. We can still have concurrency and threading using async code as you'll see later in this book.

Rune is now in a state where I want people to poke at it. Not too hard mind you. It's still early days. The compiler is very much in flux and a miscompilation will definitely cause the wrong closure to be called. You know, the one that doesn't perform your security checks ๐Ÿ˜….

But the more poking and prodding people do, the more issues will be found. Every solved issue brings Rune one step close to being production ready. Every set of eyeballs that takes a look can bring fresh perspective and ideas, making the project better for me and everyone else.

I really want to thank Jonathan Turner and all the contributors to the Rhai project. They have been an an immense inspiration to me.

You can find the project on its GitHub page. I hope you'll enjoy using it as much as I've enjoyed making it!

โ€” John-John Tedro

Introduction

Welcome the The Rune Programming Language, a reference guide that will familiarize yourself with Rune.

Rune is an open source embeddable dynamic programming language that compiles and runs on a virtual machine called Runestick (thanks Brendan).

The goal of Rune is to reimagine Rust as a dynamic programming language. Trying to mimic as many concepts as possible, and remixing the ones which do not translate directly. We do this by using the same syntax as Rust. But a few additions are inevitable because certain things are just done differently when you have a dynamic environment.

I also concede that a number of program correctness features you get through static typing will be sorely lacking. The tradeoff you get for this are fast compilation times and duck typing, sometimes leading to more concise and compact code. Python is a great example of this, and is along with Rhai and Lua biggest inspirations for this project.

To read this book, you will definitely want to go to the GitHub project and grab yourself a clone of it. All the examples are in there, and it's highly recommended that you run and tinker them yourself as you encounter them.

With that out of the way, let's get started. We have a bit to go through.

Getting Started

The first thing you need to learn about in Rune is the dbg function. This is used to "debug" values provided to it in order to understand them. Anything can be provided to it, and it will do its best to describe it.

fn function() {
    42
}

let a = [1, 2, 3];
let b = 'ไปŠ';
let closure = || println!("Hello");

dbg!(a);
dbg!(b);
dbg!(function);
dbg!(drop);
dbg!(closure);

Note: by convention Rune uses files ending in .rn.

$> cargo run -- run scripts/book/getting_started/dbg.rn
[1, 2, 3]
'ไปŠ'
dynamic function (at: 0x1a)
native function (0x1bd03b8ee40)
dynamic function (at: 0x17)

The default dbg implementation outputs information on its arguments to stdout. But its exact behavior can differ depending on how the environment is configured. When Rune is embedded into a larger application it might for example be more suitable to output to a log file.

Rune also provides print! and println! macros which can be used to format directly to stdout, but these cannot be relied on to be present to the same degree as dbg. However for our purposes we will be using rune-cli, which has all of these modules installed. This is also what was used to run the above code.

So for a more formal introduction, here is the official Rune "Hello World":

println!("Hello World");
$> cargo run -- run scripts/book/getting_started/hello_world.rn
Hello World

So now you know how to run Rune scripts. Well done! Let's move on to the next chapter.

Concepts

This chapter covers common concepts that appear in almost all programming languages, and how they work in Rune.

Hopefully these should be familiar to anyone who's used imperative programming languages before. We'll try to take each concept and describe how they work with examples, one at a time.

Items and imports

Everything in Rune has a unique name. Every function, type, and import. This name is what identifies that thing, and is called its item. Rune performs compile time checks to make sure that every item we try to use actually exists.

The following are examples of items in Rune:

  • std::result::Result (a type)
  • std::iter::range (a function)

The first refers to the Result enum, and the second is the range function. They both live within their corresponding std module. Result is a bit special even, since it's part of the prelude, allowing us to use it without importing it. But what about range?

If we wanted to use range we would have to import it first with a use statement:

use std::iter::once;

let it = once(0);

dbg!(it.next());
dbg!(it.next());
$> cargo run -- run scripts/book/items_imports/example_import.rn
std::iter::Range

Trying to use an item which doesn't exist results in a compile error:

pub fn main() {
    let foo = Foo::new();
}
$> cargo run -- run scripts/book/items_imports/missing_item.rn.fail
error: compile error
  โ”Œโ”€ scripts/book/items_imports/missing_item.rn.fail:2:15
  โ”‚
2 โ”‚     let foo = Foo::new();
  โ”‚               ^^^^^^^^ missing item `Foo::new`

Every item used in a Rune program must be known at compile time. This is one of the static guarantees every Rune script are has to fulfill. And is one important point where it differs from Lua or Python.

Modules

Rune has support for modules purely defined in Rune itself. This is done using the mod keyword. And the module can either be loaded from a different file matching the name of the module or defined directly inside of the source file.

The following is an example of an inline module:

mod foo {
    pub fn number() {
        1
    }
}

mod bar {
    pub fn number() {
        2
    }
}

pub fn main() {
    dbg!(foo::number() + bar::number());
}
$> cargo run -- run scripts/book/items_imports/inline_modules.rn
3

And this is the equivalent modules loaded from the filesystem. These are three separate files:

mod foo;
mod bar;

dbg!(foo::number() + bar::number());
// file: ./foo/mod.rn
pub fn number() {
    2
}
// file: ./bar.rn
pub fn number() {
    1
}
$> cargo run -- run scripts/book/items_imports/modules.rn
3

Disambiguating imports

Normally an item would simply be used through its local name, such as foo::number above. But what happens if we need to reference a module which is not a direct descendent of the current one or there is some ambiguation?

To this end Rune supports the following Rust keywords:

  • self - which will resolve items from the root of the current module.
  • crate - which will look up items from the entrypoint of the current project.
  • super - which will look up items from the parent of the current module.
mod first {
    pub fn number() {
        crate::number() + 2
    }
}

mod second {
    pub fn number() {
        super::first::number() + 4
    }
}

pub fn number() {
    1
}

dbg!(self::second::number());
$> cargo run -- run scripts/book/items_imports/item_keywords.rn
7

Visibility

Every item used has to be visible to that item. This is governed by Runes visibility rules, which are the following:

  • An item can have inherited (empty) or a specified visibility like pub or pub(crate).
  • For an item to be visible, all of its parent items have to be visible.
  • Items with inherited visibility are equivalent to pub(self), making the item only visible in the module in which they are defined.

The available visibility modifiers are:

  • pub - the item is visible from anywhere.
  • pub(crate) - the item is visible in the same crate.
  • pub(super) - the item is visible in the parent item only.
  • pub(self) - the item is only visible to other items in the same module.
  • pub(in path) - the item is only visible in the specified path. This is not supported yet.

Note that Rune doesn't have support for crates yet, meaning pub(crate) and pub are currently effectively equivalent.

Functions

One of the most common things in all of programming are functions. These are stored procedures which take a arguments, do some work, and then return. Functions are used because they encapsulate what they do so that the programmer only needs to concern itself with the protocol of the function.

What does it do? What kind of arguments does it take? The alternative would be to copy the code around and that wouldn't be very modular. Functions instead provide a modular piece of code that can be called and re-used. Over and over again.

fn keyword

In Rune, functions are declared with the fn keyword. You've already seen one which is used in every example, main. This is not a special function, but is simply what the Rune cli looks for when deciding what to execute.

pub fn main() {
    println!("Hello World");
}
$> cargo run -- run scripts/book/functions/main_function.rn
Hello World

In Rune, you don't have to specify the return type of a function. Given that Rune is a dynamic programming language, this allows a function to return anything, even completely distinct types.

fn foo(condition) {
    if condition {
        "Hello"
    } else {
        1
    }
}

pub fn main() {
    println!("{}", foo(true));
    println!("{}", foo(false));
}
$> cargo run -- run scripts/book/functions/return_value.rn
Hello
1

Depending on who you talk to, this is either the best thing since sliced bread or quite scary. It allows for a larger ability to express a program, but at the same time it can be harder to reason on what your program will do.

Calling functions in Rust

Rune functions can be easily set up and called from Rust.

use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Vm};

use std::sync::Arc;

fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;

    let mut sources = rune::sources!(
        entry => {
            pub fn main(number) {
                number + 10
            }
        }
    );

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(Arc::new(context.runtime()?), Arc::new(unit));
    let output = vm.execute(["main"], (33i64,))?.complete().into_result()?;
    let output: i64 = rune::from_value(output)?;

    println!("output: {}", output);
    Ok(())
}
$> cargo run --example minimal
output: 43

Control Flow

Rune supports a number of control flow expressions. We will be dedicating this section to describe the most common ones.

return expression

In the previous section we talked about functions. And one of the primary things a function does is return things. The return expression allows for returning from the current function. If used without an argument, the function will return a unit ().

The last statement in a function is known as an implicit return, and will be what the function returns by default unless a return is specified.

fn foo(n) {
    if n < 1 {
        return "less than one";
    }

    "something else"
}

println!("{}", foo(0)); // => outputs: "less than one"
println!("{}", foo(10)); // => outputs: "something else"
$> cargo run -- run scripts/book/control_flow/numbers_game.rn
less than one
something else

if expressions

If expressions allow you to provide a condition with one or more code branches. If the condition is true, the provided block of code will run.

let number = 3;

if number < 5 {
    println!("The number is smaller than 5");
}
$> cargo run -- run scripts/book/control_flow/conditional.rn
The number *is* smaller than 5

Optionally, we can add another branch under else, which will execute in case the condition is false.

let number = 3;

if number < 5 {
    println!("the number is smaller than 5");
} else {
    println!("the number is 5 or bigger");
}
$> cargo run -- run scripts/book/control_flow/conditional_else.rn
the number is smaller than 5

We can also add an arbitrary number of else if branches, which allow us to specify many different conditions.

let number = 3;

if number < 5 {
    println!("the number is smaller than 5");
} else if number == 5 {
    println!("the number is exactly 5");
} else {
    println!("the number is bigger than 5");
}
$> cargo run -- run scripts/book/control_flow/conditional_else_ifs.rn
the number is smaller than 5

Do note however that if you have many conditions, it might be cleaner to use a match.

This will be covered in a later section, but here is a sneak peek:

let number = 3;

match number {
    n if n < 5 => {
        println!("the number is smaller than 5");
    }
    5 => {
        println!("the number is exactly 5");
    }
    n => {
        println!("the number is bigger than 5");
    }
}
$> cargo run -- run scripts/book/control_flow/first_match.rn
the number is smaller than 5

Variables and memory

Variables in Rune are defined using the let keyword. In contrast to Rust, all variables in Rune are mutable and can be changed at any time.

pub fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}
$> cargo run -- run scripts/book/variables/variables.rn
The value of x is: 5
The value of x is: 6

Rune is a memory safe language. Regardless of what you write in a Rune script, we maintain the same memory safety guarantees as safe Rust. This is accomplished through reference counting.

Unless a value is Copy, they are reference counted and can be used at multiple locations. This means that they have shared ownership. Every variable that points to that value therefore points to the same instance of that value. You can think of every nontrivial value being automatically wrapped in an Rc<RefCell<T>> if that helps you out.

This is not exactly what's going on. If you're interested to learn more, Rune uses a container called Shared<T> which is like an Rc<RefCell<T>>, but has a few more tricks.

We can see how this works by sharing and mutating one object across two variables:

pub fn main() {
    let object = #{ field: 1 };
    let object2 = object;
    println!("{}", object.field);
    object2.field = 2;

    // Note: we changed `object2`, but read out `object`
    println!("{}", object.field);
}
$> cargo run -- run scripts/book/variables/shared_ownership.rn
1
2

This can cause issues if we call an external function which expects to take ownership of its arguments. We say that functions like these move their argument, and if we try to use a variable which has been moved an error will be raised in the virtual machine.

Note: Below we use the drop function, which is a built-in function that will take its argument and free it.

pub fn main() {
    let object = #{ field: 1 };
    let object2 = object;
    println!("field: {}", object.field);
    drop(object2);
    println!("field: {}", object.field);
}
$> cargo run -- run scripts/book/variables/take_argument.rn
field: 1
== ! (cannot read, value is moved (at 14)) (469ยตs)
error: virtual machine error
  โ”Œโ”€ scripts/book/variables/take_argument.rn:6:27
  โ”‚
6 โ”‚     println!("field: {}", object.field);
  โ”‚                           ^^^^^^^^^^^^ cannot read, value is moved

If you need to, you can test if a variable is still accessible for reading with is_readable, and for writing with is_writable. These are both imported in the prelude. An object which is writable is also movable, and can be provided to functions which need to move the value, like drop.

pub fn main() {
    let object = #{ field: 1 };
    let object2 = object;
    println!("field: {}", object.field);
    drop(object2);

    if is_readable(object) {
        println!("field: {}", object.field);
    } else {
        println!("object is no longer readable ๐Ÿ˜ข");
    }
}
$> cargo run -- run scripts/book/variables/is_readable.rn
field: 1
object is no longer readable ๐Ÿ˜ข

Loops

Loops are a fundamental building block common to many programming languages. This is no exception in Rune. Loops allow you to execute a block of code until a specific condition is reached, which can be a powerful tool for accomplishing programming tasks.

break Keyword

Every loop documented in this section can be terminated early using the break keyword.

When Rune encounters a break, it will immediately jump out of the loop it is currently in and continue running right after it.

let value = 0;

while value < 100 {
    if value >= 50 {
        break;
    }

    value = value + 1;
}

println!("The value is {}", value); // => The value is 50
$> cargo run -- run scripts/book/loops/while_loop.rn
The value is 50

loop Expressions

The loop keyword builds the most fundamental form of loop in Rune. One that repeats unconditionally forever, until it is exited using another control flow operator like a break or a return.

use time::Duration;

loop {
    println!("Hello forever!");
    time::sleep(Duration::from_secs(1)).await;
}
$> cargo run -- run scripts/book/loops/loop_forever.rn
Hello forever!
Hello forever!
Hello forever!
...

Hint: If you want this one to end, you're gonna have to kill it with CTRL+C.

We're also using an asynchronous function called sleep above to avoid spamming our terminals too much. Well talk more about these in a later section.

When broken out of, loops produce the value provided as an argument to the break keyword. By default, this is simply a unit ().

let counter = 0;

let total = loop {
    counter = counter + 1;

    if counter > 10 {
        break counter;
    }
};

println!("The final count is: {}", total);
$> cargo run -- run scripts/book/loops/loop_break.rn
The final count is: 11

Pattern matching

In this section we will be discussing Pattern Matching.

Pattern matching is a flexible mechanism that allows for validating the structure and type of the argument, while also destructuring it to give easy access to what you need.

Below are some examples of its common uses to match on branch conditions:

fn match_input(n) {
    match n {
        1 => println!("The number one."),
        n if n is i64 => println!("Another number: {}.", n),
        [1, 2, n, ..] => println!("A vector starting with one and two, followed by {}.", n),
        "one" => println!("One, but this time as a string."),
        _ => println!("Something else. Can I go eat now?"),
    }
}

match_input(1);
match_input(2);
match_input([1, 2, 42, 84]);
match_input("one");
match_input(#{ field: 42 });
$> cargo run -- run scripts/book/pattern_matching/big_match.rn
The number one.
Another number: 2.
A vector starting with one and two, followed by 42.
One, but this time as a string.
Something else. Can I go eat now?

We will be covering each of these variants in detail in the coming sections.

Patterns

Things that can be matched over are called patterns, and there's a fairly large number of them. In this section we'll try to document the most common ones.

Patterns that can be matched over are the following:

  • A unit, simply ().
  • A boolean value, like true or false.
  • A byte, like b'a' or b'\x10'.
  • A character, like 'a' or 'ใ‚'.
  • An integer, like 42.
  • A string, like "Steven Universe".
  • A vector, like the numbers [1, _, ..], or simply the empty vector []. The values in the vectors are patterns themselves.
  • A tuple, like ("Steven Universe", _, 42). The values in the tuple are patterns themselves.
  • An object, like the numbers {"name": "Steven Universe", "age": _}, or the empty {}. The values in the object are patterns themselves.

Structs can be matched over by prefixing the match with their name:

  • A unit struct: Foo.
  • A tuple struct: Foo(1, _).
  • An object struct: Foo { bar: 1, .. }.

Similarly, variants in an enum can be matched over as well in the same way:

  • A unit variant: Foo::Variant.
  • A tuple variant: Foo::Variant(1, _).
  • An object variant: Foo::Variant { bar: 1, .. }.

Patterns can be almost any combination of the above. Even {"items": ["Sword", "Bow", "Axe"]} is a pattern that can be matched over.

Anything that qualifies as a collection can have .. as a suffix to match the case that there are extra fields or values which are not covered in the pattern. This is called a rest pattern.

let value = #{ a: 0, b: 1 };

let matched = match value {
    // this doesn't match, because a pattern without a rest pattern in it
    // must match exactly.
    #{ a } => false,
    // this matches, because it only requires `a` to be present.
    #{ a, .. } => true,
};

assert!(matched, "rest pattern matched");
$> cargo run -- run scripts/book/pattern_matching/rest_pattern.rn

Binding and ignoring

In a pattern, every value can be replaced with a binding or an ignore pattern. The ignore pattern is a single underscore _, which informs Rune that it should ignore that value, causing it to match unconditionally regardless of what it is.

fn test_ignore(vector) {
    match vector {
        [_, 2] => println!("Second item in vector is 2."),
    }
}

test_ignore([1, 2]);
$> cargo run -- run scripts/book/pattern_matching/ignore.rn
Second item in vector is 2.

In contrast to ignoring, we can also bind the value to a variable that is then in scope of the match arm. This will also match the value unconditionally, but give us access to it in the match arm.

fn test_ignore(vector) {
    match vector {
        [_, b] => println!("Second item in vector is {}.", b),
    }
}

test_ignore([1, 2]);
$> cargo run -- run scripts/book/pattern_matching/bind.rn
Second item in vector is 2.

Here are some more examples:

  • [_, a, b] which will ignore the first value in the vector, but then bind the second and third as a and b.
  • {"name": name} will bind the value name out of the specified object.
fn describe_car(car) {
    match car {
        #{ "make": year, .. } if year < 1950 => "What, where did you get that?",
        #{ "model": "Ford", "make": year, .. } if year >= 2000 => "Pretty fast!",
        _ => "Can't tell ๐Ÿ˜ž",
    }
}

println!("{}", describe_car(#{"model": "Ford", "make": 2000}));
println!("{}", describe_car(#{"model": "Honda", "make": 1980}));
println!("{}", describe_car(#{"model": "Volvo", "make": 1910}));
$> cargo run -- run scripts/book/pattern_matching/fast_cars.rn
Pretty fast!
Can't tell ๐Ÿ˜ž
What, where did you get that?

Template literals

If you've been paying attention on previous sections you might have seen odd looking strings like `Hello ${name}`. These are called template literals, and allow you to conveniently build strings using variables from the environment.

Template literals are a concept borrowed from EcmaScript.

let age = 30;
dbg!(`I am ${age} years old!`);
$> cargo run -- run scripts/book/template_literals/basic_template.rn
"I am 30 years old!"

Template strings are accelerated by the Vm, each argument uses a display protocol and it can be very efficient to build complex strings out of it.

The DISPLAY_FMT protocol

The DISPLAY_FMT protocol is a function that can be implemented by any external type which allows it to be used in a template string.

It expects a function with the signature fn(&self, buf: &mut String) -> fmt::Result.

use rune::{ContextError, Module};
use rune::runtime::{Protocol, Formatter};
use std::fmt::Write as _;
use std::fmt;

#[derive(Debug)]
pub struct StatusCode {
    inner: u32,
}

impl StatusCode {
    #[rune::function(protocol = DISPLAY_FMT)]
    fn display_fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}", self.inner)
    }
}

pub fn module() -> Result<Module, ContextError> {
    let mut module = Module::new(["http"]);
    module.function_meta(StatusCode::display_fmt)?;
    Ok(module)
}

This is what allows status codes to be formatted into template strings, any types which do not implement this protocol will fail to run.

let vec = [1, 2, 3];
dbg!(`${vec}`);
$> cargo run -- run scripts/book/template_literals/not_a_template.rn
== ! (`Vec` does not implement the `display_fmt` protocol (at 5)) (77.7ยตs)
error: virtual machine error
  โ”Œโ”€ scripts/book/template_literals/not_a_template.rn:3:9
  โ”‚
3 โ”‚     dbg!(`${vec}`);
  โ”‚          ^^^^^^^^ `Vec` does not implement the `display_fmt` protocol

Instance functions

Instance functions are functions that are associated to a specific type of variable. When called they take the form value.foo(), where the instance is the first part value. And the instance function is foo().

These are a bit special in Rune. Since Rune is a dynamic programming language we can't tell at compile time which instance any specific value can be. So instance functions must be looked up at runtime.

struct Foo;

impl Foo {
    fn new() {
        Foo
    }
}

let foo = Foo::new();
foo.bar();
$> cargo run -- run scripts/book/instance_functions/missing_instance_fn.rn
error: virtual machine error
   โ”Œโ”€ scripts/book/instance_functions/missing_instance_fn.rn:11:5
   โ”‚
11 โ”‚     foo.bar();
   โ”‚     ^^^^^^^^^ missing instance function `0xfb67fa086988a22d` for `type(0xc153807c3ddc98d7)``

Note: The error is currently a bit nondescript. But in the future we will be able to provide better diagnostics by adding debug information.

What you're seeing above are type and function hashes. These uniquely identify the item in the virtual machine and is the result of a deterministic computation based on its item. So the hash for the item Foo::new will always be the same.

In Rust, we can calculate this hash using the Hash::type_hash method:

use rune::{Hash, ItemBuf};

fn main() -> rune::support::Result<()> {
    println!("{}", Hash::type_hash(&ItemBuf::with_item(["Foo", "new"])?));
    println!("{}", Hash::type_hash(["Foo", "new"]));
    Ok(())
}
$> cargo run --example function_hash
0xb5dc92ab43cb37d9
0xb5dc92ab43cb37d9

The exact implementation of the hash function is currently not defined, but will be stabilized and documented in a future release.

Defining instance functions in Rust

Native instance functions are added to a runtime environment using the Module::associated_function function. The type is identified as the first argument of the instance function, and must be a type registered in the module using Module::ty.

use rune::termcolor::{ColorChoice, StandardStream};
use rune::{ContextError, Diagnostics, Module, Vm};

use std::sync::Arc;

#[rune::function(instance)]
fn divide_by_three(value: i64) -> i64 {
    value / 3
}

#[tokio::main]
async fn main() -> rune::support::Result<()> {
    let m = module()?;

    let mut context = rune_modules::default_context()?;
    context.install(m)?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources!(entry => {
        pub fn main(number) {
            number.divide_by_three()
        }
    });

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(runtime, Arc::new(unit));
    let output = vm.execute(["main"], (33i64,))?.complete().into_result()?;
    let output: i64 = rune::from_value(output)?;

    println!("output: {}", output);
    Ok(())
}

fn module() -> Result<Module, ContextError> {
    let mut m = Module::with_item(["mymodule"])?;
    m.function_meta(divide_by_three)?;
    Ok(m)
}
$> cargo run --example custom_instance_fn
output: 11

For more examples on how modules can be used you can have a look at the source for the rune-modules crate.

Field functions

Field functions are special operations which operate on fields. These are distinct from associated functions, because they are invoked by using the operation associated with the kind of the field function.

The most common forms of fields functions are getters and setters, which are defined through the Protocol::GET and Protocol::SET protocols.

The Any derive can also generate default implementations of these through various #[rune(...)] attributes:

#[derive(Any)]
struct External {
    #[rune(get, set, add_assign, copy)]
    number: i64,
    #[rune(get, set)]
    string: String,
}

Once registered, this allows External to be used like this in Rune:

pub fn main(external) {
    external.number = external.number + 1;
    external.number += 1;
    external.string = `${external.string} World`;
}

The full list of available field functions and their corresponding attributes are:

ProtocolAttribute
Protocol::GET#[rune(get)]For getters, like external.field.
Protocol::SET#[rune(set)]For setters, like external.field = 42.
Protocol::ADD_ASSIGN#[rune(add_assign)]The += operation.
Protocol::SUB_ASSIGN#[rune(sub_assign)]The -= operation.
Protocol::MUL_ASSIGN#[rune(mul_assign)]The *= operation.
Protocol::DIV_ASSIGN#[rune(div_assign)]The /= operation.
Protocol::BIT_AND_ASSIGN#[rune(bit_and_assign)]The &= operation.
Protocol::BIT_OR_ASSIGN#[rune(bit_or_assign)]The bitwise or operation.
Protocol::BIT_XOR_ASSIGN#[rune(bit_xor_assign)]The ^= operation.
Protocol::SHL_ASSIGN#[rune(shl_assign)]The <<= operation.
Protocol::SHR_ASSIGN#[rune(shr_assign)]The >>= operation.
Protocol::REM_ASSIGN#[rune(rem_assign)]The %= operation.

The manual way to register these functions is to use the new Module::field_function function. This clearly showcases that there's no relationship between the field used and the function registered:

use rune::{Any, Module};
use rune::runtime::Protocol;

#[derive(Any)]
struct External {
}

impl External {
    fn field_get(&self) -> String {
        String::from("Hello World")
    }
}

let mut module = Module::new();
module.field_function(&Protocol::GET, "field", External::field_get)?;

Would allow for this in Rune:

pub fn main(external) {
    println!("{}", external.field);
}

Custom field function

Using the Any derive, you can specify a custom field function by using an argument to the corresponding attribute pointing to the function to use instead.

The following uses an implementation of add_assign which performs checked addition:

use rune::runtime::{VmError, VmResult};
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Any, ContextError, Diagnostics, Module, Vm};

use std::sync::Arc;

#[derive(Any)]
struct External {
    #[rune(add_assign = External::value_add_assign)]
    value: i64,
}

#[allow(clippy::unnecessary_lazy_evaluations)]
impl External {
    fn value_add_assign(&mut self, other: i64) -> VmResult<()> {
        self.value = rune::vm_try!(self.value.checked_add(other).ok_or_else(VmError::overflow));
        VmResult::Ok(())
    }
}

fn main() -> rune::support::Result<()> {
    let m = module()?;

    let mut context = rune_modules::default_context()?;
    context.install(m)?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            pub fn main(e) {
                e.value += 1;
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(runtime, Arc::new(unit));

    let input = External { value: i64::MAX };
    let err = vm.call(["main"], (input,)).unwrap_err();
    println!("{:?}", err);
    Ok(())
}

fn module() -> Result<Module, ContextError> {
    let mut m = Module::new();
    m.ty::<External>()?;
    Ok(m)
}
$> cargo run --example checked_add_assign
Error: numerical overflow (at inst 2)

Traits

Traits in rune defines a collection associated items. Once a trait is implemented by a type we can be sure that all the associated names it defines are present on the type.

Traits allow us to reason about types more abstractly, such as this is an iterator.

Limits

As usual, Rune doesn't permit more than one definition of an associated name. Attempting to define more than one with the same name results in a build-time error. This is in contrast to Rust which allows multiple traits with overlapping methods to be defined. So why doesn't Rune allow for this?

Since Rune is a dynamic language, consider what would happen in a situation like this:

#![allow(unused)]
fn main() {
struct Foo {
    /* .. */
}

impl Iterator for Foo {
    fn next(self) {
        /* .. */
    }
}

impl OtherIterator for Foo {
    fn next(self) {
        /* .. */
    }
}

let foo = Foo {
    /* .. */
};

// Which implementation of `next` should we call?
while let Some(value) = foo.next() {

}
}

Since there are no type parameters we can't solve the ambiguity by either only having one trait defining the method in scope or by using an unambigious function qualified function call.

Implementation

In the background the user-facing implementation of traits is done by implementing protocols just before. Protocols are still used by the virtual machine to call functions.

The separation that protocols provide is important because we don't want a user to accidentally implement an associated method which would then be picked up by a trait. Protocols are uniquely defined in their own namespace and cannot be invoked in user code.

As an example, to implement the Iterator trait you have to implement the NEXT protocol. So if the NEXT protocol is present and we request that the ::std::iter::Iterator trait should be implemented, the NEXT protocol implementation is used to construct all the relevant associated methods. This is done by calling Module::implement_trait.

#![allow(unused)]
fn main() {
let mut m = Module::with_item(["module"]);
m.ty::<Iter>()?;
m.function_meta(Iter::next__meta)?;
m.function_meta(Iter::size_hint__meta)?;
m.implement_trait::<Iter>(rune::item!(::std::iter::Iterator))?;

#[derive(Any)]
#[rune(item = "module")]
struct Iter {
    /* .. */
}

impl Iter {
    #[rune::function(keep, protocol = NEXT)]
    fn size_hint(&self) -> Option<bool> {
        Some(true)
    }

    #[rune::function(keep, protocol = SIZE_HINT)]
    fn size_hint(&self) -> (usize, Option<usize>) {
        (1, None)
    }
}
}

Note that this allows the Iter type above to specialize its SIZE_HINT implementation. If the SIZE_HINT protocol was not defined, a default implementation would be provided by the trait.

As a result of implementing the ::std::iter::Iterator trait, the Iter type now automatically gets all the iterator-associated function added to it. So not only can you call Iter::next to advance the iterator, but also make use of combinators such as filter:

#![allow(unused)]
fn main() {
let it = /* construct Iter */;

for value in it.filter(|v| v != true) {
    dbg!(value);
}
}

Defining a trait

Defining a trait is currently a low-level module operation. It's done by implementing a handler which will be called to populate the relevant methods when the trait is implement. Such as this snippet for the Iterator trait:

#![allow(unused)]
fn main() {
let mut m = Module::with_crate("std", ["iter"]);

let mut t = m.define_trait(["Iterator"])?;

t.handler(|cx| {
    let next = cx.find(&Protocol::NEXT)?;

    let size_hint = cx.find_or_define(&Protocol::SIZE_HINT, |_: Value| (0usize, None::<usize>))?;

    /* more methods */
    Ok(())
})?;
}

Calling find requires that NEXT is implemented. We can also see that the implementation for SIZE_HINT will fall back to a default implementation if it's not implemented. The appropriate protocol is also populated if it's missing. All the relevant associated functions are also provided, such as value.next() and value.size_hint().

Rune types

Types in Rune are identified uniquely by their item. An item path is a scope-separated identifier, like std::f64. This particular item identifies a type.

These items can be used to perform basic type checking using the is and is not operations, like this:

assert!(() is Tuple, "tuples should be tuples");
assert!((1, 2) is Tuple, "tuples should be tuples");
assert!(true is bool, "bools should be bools");
assert!('a' is char, "chars should be chars");
assert!(b'a' is u64, "bytes should be unsigned integers");
assert!(42 is i64, "integers should be integers");
assert!(42.1 is f64, "floats should be floats");
assert!("hello" is String, "strings should be strings");
assert!("x" is not char, "strings are not chars");
assert!(#{"hello": "world"} is Object, "objects should be objects");
assert!(["hello", "world"] is Vec, "vectors should be vectors");
$> cargo run -- run scripts/book/types/types.rn

Conversely, the type check would fail if you're providing a value which is not of that type.

assert!(["hello", "world"] is String, "vectors should be strings");
$> cargo run -- run scripts/book/types/bad_type_check.rn
== ! (panicked `assertion failed: vectors should be strings` (at 12)) (133.3ยตs)
error: virtual machine error
  โ”Œโ”€ scripts/book/types/bad_type_check.rn:2:5
  โ”‚
2 โ”‚     assert!(["hello", "world"] is String, "vectors should be strings");
  โ”‚     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ panicked `assertion failed: vectors should be strings`

This gives us insight at runtime which type is which, and allows Rune scripts to make decisions depending on what type a value has.

fn dynamic_type(n) {
    if n is String {
        "n is a String"
    } else if n is Vec {
        "n is a vector"
    } else {
        "n is unknown"
    }
}

println!("{}", dynamic_type("Hello"));
println!("{}", dynamic_type([1, 2, 3, 4]));
println!("{}", dynamic_type(42));
$> cargo run -- run scripts/book/types/type_check.rn
n is a String
n is a vector
n is unknown

A tighter way to accomplish this would be by using pattern matching, a mechanism especially suited for many conditional branches. Especially when the branches are different types or variants in an enum.

fn dynamic_type(n) {
    match n {
        n if n is String => "n is a String",
        n if n is Vec => "n is a vector",
        _ => "n is unknown",
    }
}

println!("{}", dynamic_type("Hello"));
println!("{}", dynamic_type([1, 2, 3, 4]));
println!("{}", dynamic_type(42));
$> cargo run -- run scripts/book/types/type_check_patterns.rn
n is a String
n is a vector
n is unknown

Primitive and reference types

Primitives are values stored immediately on the stack. In Rust terminology, these types are Copy, so reassigning them to different values will create distinct copies of the underlying value.

The primitives available in Rune are:

  • The unit ().
  • Booleans, true and false.
  • Bytes, like b'\xff'.
  • Characters, like 'ไปŠ'. Which are 4 byte wide characters.
  • Integers, like 42. Which are 64-bit signed integers.
  • Floats, like 3.1418. Which are 64-bit floating point numbers.
  • Static strings, like "Hello World".
  • Type hashes.

You can see that these bytes are Copy when assigning them to a different variable, because a separate copy of the value will be used automatically.

let a = 1;
let b = a;
a = 2;
println!("{a}");
println!("{b}");
$> cargo run -- run scripts/book/primitives/copy.rn
2
1

Other types like strings are stored by reference. Assigning them to a different variable will only copy their reference, but they still point to the same underlying data.

let a = String::from("Hello");
let b = a;
a.push_str(" World");
println!("{a}");
println!("{b}");
$> cargo run -- run scripts/book/primitives/primitives.rn
Hello World
Hello World

Vectors

A vector is a native data structure of Rune which is a dynamic list of values. A vector isn't typed, and can store any Rune values.

pub fn main() {
    let values = ["Hello", 42];

    println!("{}", values[0]);
    println!("{}", values.1); // items in vectors can be accessed like tuple fields.

    for v in values {
        println!("{}", v);
    }
}
$> cargo run -- run scripts/book/vectors/vectors.rn
Hello
42
Hello
42

As you can see, you can iterate over a vector because it implements the iterator protocol. It is also possible to create and use an iterator manually using Vec::iter, giving you more control over it.

pub fn main() {
    let values = ["Hello", 42];

    for v in values.iter().rev() {
        println!("{}", v);
    }
}
$> cargo run -- run scripts/book/vectors/vectors_rev.rn
42
Hello

Using vectors from Rust

Vectors are represented externally as the standard Vec.

use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Vm};

use std::sync::Arc;

fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            pub fn calc(input) {
                let output = 0;

                for value in input {
                    output += value;
                }

                [output]
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;
    let mut vm = Vm::new(runtime, Arc::new(unit));

    let input = vec![1, 2, 3, 4];
    let output = vm.call(["calc"], (input,))?;
    let output: Vec<i64> = rune::from_value(output)?;

    println!("{:?}", output);
    Ok(())
}
$> cargo run --example vector
[10]

If you have a vector which has values of non-uniform types, you can use VecTuple to deal with them.

use rune::runtime::VecTuple;
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Vm};

use std::sync::Arc;

fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            pub fn calc(input) {
                let a = input[0] + 1;
                let b = format!("{} World", input[1]);
                [a, b]
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;
    let mut vm = Vm::new(runtime, Arc::new(unit));

    let input: VecTuple<(i64, String)> = VecTuple::new((1, String::from("Hello")));
    let output = vm.call(["calc"], (input,))?;
    let VecTuple((a, b)): VecTuple<(u32, String)> = rune::from_value(output)?;

    println!("({:?}, {:?})", a, b);
    Ok(())
}
$> cargo run --example vec_tuple
(2, "Hello World")

Objects

Objects are anonymous maps, which support defining and using arbitrary string keys.

let values = #{};
values["first"] = "bar";
values["second"] = 42;

dbg!(values["first"]);
dbg!(values.second); // items be accessed like struct fields.

if let Some(key) = values.get("not a key") {
    dbg!(key);
} else {
    println!("key did not exist");
}

for entry in values {
    dbg!(entry);
}
$> cargo run -- run scripts/book/objects/objects.rn
"bar"
42
key did not exist
("second", 42)
("first", "bar")

These are useful because they allow their data to be specified dynamically, which is exactly the same use case as storing unknown JSON.

One of the biggest motivations for Rune to have anonymous objects is so that we can natively handle data with unknown structure.

async fn get_commits(repo, limit) {
    let limit = limit.unwrap_or(10);

    let client = http::Client::new();
    let request = client.get(`https://api.github.com/repos/${repo}/commits`);
    let response = request.header("User-Agent", "Rune").send().await?;
    let text = response.text().await?;
    let json = json::from_string(text)?;

    let commits = json.iter().take(limit).map(|e| e.sha).collect::<Vec>();
    Ok(commits)
}

for commit in get_commits("rune-rs/rune", Some(5)).await? {
    println!("{commit}");
}
$> cargo run -- run scripts/book/objects/json.rn
9c4bdaf194410d8b2f5d7f9f52eb3e64709d3414
06419f2580e7a18838f483321055fc06c0d75c4c
cba225dad143779a0a9543cfb05cde9710083af5
15133745237c014ff8bae53d8ff8f3c137c732c7
39ac97ab4ebe26118e807eb91c7656ab95b1fcac
3f6310eeeaca22d0373cc11d8b34d346bd12a364

Using objects from Rust

Objects are represented externally as the Object type alias. The keys are always strings, but its value must be specified as the sole type parameter. Note that the dynamic Value can be used if the type is unknown.

use rune::alloc;
use rune::runtime::Object;
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Vm};

use std::sync::Arc;

fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            pub fn calc(input) {
                dbg(input["key"]);
                input["key"] = "World";
                input
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(runtime, Arc::new(unit));

    let mut object = Object::new();
    object.insert(alloc::String::try_from("key")?, rune::to_value(42i64)?)?;

    let output = vm.call(["calc"], (object,))?;
    let output: Object = rune::from_value(output)?;

    println!("{:?}", output.get("key"));
    Ok(())
}
$> cargo run --example object
42
Some("World")

Tuples

Tuples in Rune are fixed-size sequences of values. Similarly to a vector, tuples can contain any sequence of values. But there's no way to change the size of a tuple.

let values = ("Now", "You", "See", "Me");
dbg!(values);

values.2 = "Don't";
values.3 = "!";
dbg!(values);
$> cargo run -- run scripts/book/tuples/tuple_masquerade.rn
("Now", "You", "See", "Me")
("Now", "You", "Don\'t", "!")

The following is a simple example of a function returning a tuple:

fn foo() {
    (1, "test")
}

dbg!(foo());
$> cargo run -- run scripts/book/tuples/basic_tuples.rn
(1, "test")

Tuples can also be pattern matched:

match ("test", 1) {
    ("test", n) => {
        dbg!("the first part was a number:", n);
    }
    _ => {
        dbg!("matched something we did not understand");
    }
}
$> cargo run -- run scripts/book/tuples/tuple_patterns.rn
"the first part was a number:"
1

Using tuples from Rust

Tuples are represented externally as primitive tuple types.

use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Vm};

use std::sync::Arc;

fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            pub fn calc(input) {
                (input.0 + 1, input.1 + 2)
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(runtime, Arc::new(unit));
    let output = vm.call(["calc"], ((1u32, 2u32),))?;
    let output: (i32, i32) = rune::from_value(output)?;

    println!("{:?}", output);
    Ok(())
}
$> cargo run --example tuple
(2, 4)

Dynamic types

Dynamic types are types which can be defined and used solely within a Rune script. They provide the ability to structure data and associate functions with it.

The following is a quick example of a struct:

struct Person {
    name,
}

impl Person {
    fn greet(self) {
        println!("Greetings from {}, and good luck with this section!", self.name);
    }
}

let person = Person { name: "John-John Tedro" };

person.greet();
$> cargo run -- run scripts/book/dynamic_types/greeting.rn
Greetings from John-John Tedro, and good luck with this section!

Structs

Structs are like objects, except that they have a predefined structure with a set of keys that are known at compile time and guaranteed to be defined.

Structs can also, like most types, have an impl block associated with them which creates instance functions that you can call on an instance of that struct.

struct User {
    username,
    active,
}

impl User {
    fn set_active(self, active) {
        self.active = active;
    }

    fn describe(self) {
        if self.active {
            println!("{} is active.", self.username);
        } else {
            println!("{} is inactive.", self.username);
        }
    }
}

let user = User { username: "setbac", active: false };

user.describe();
user.set_active(true);
user.describe();
$> cargo run -- run scripts/book/structs/user_database.rn
setbac is inactive
setbac is active

Structs can also be pattern matched, like most types.

But since the fields of a struct are known at compile time, the compiler can ensure that you're only using fields which are defined.

struct User {
    username,
    active,
}

impl User {
    fn describe(self) {
        match self {
            User { username: "setbac", .. } => {
                println!("Yep, it's setbac.");
            }
            User { username, .. } => {
                println!("Other user: {username}.");
            }
        }
    }
}

let user = User { username: "setbac", active: false };

user.describe();
user.username = "newt";
user.describe();
$> cargo run -- run scripts/book/structs/struct_matching.rn
Yep, it's setbac.
Other user: newt.

Enums

Rune has support for enumerations. These allow you to define a type with zero or more variants, where each variant can hold a distinct set of data.

In a dynamic programming language enums might not seem quite as useful, but it's important for Rune to support them to have a level of feature parity with Rust.

Even so, in this section we'll explore some cases where enums are useful.

The Option enum

Rune has native support for Option, the same enum available in Rust that allows you to represent data that can either be present with Option::Some, or absent with Option::None.

fn count_numbers(limit) {
    let limit = limit.unwrap_or(10);

    for n in 0..limit {
        println!("Count: {}", n);
    }
}

println!("First count!");
count_numbers(None);

println!("Second count!");
count_numbers(Some(2));
$> cargo run -- run scripts/book/enums/count_numbers.rn
First count!
Count: 0
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
Count: 6
Count: 7
Count: 8
Count: 9
Second count!
Count: 0
Count: 1

Using an Option allows us to easily model the scenario where we have an optional function parameter, with a default fallback value.

In the next section we'll be looking into a control flow construct which gives Option superpowers.

The try operator.

External types

When a type is declared outside of Rune it is said to be external. External types are declared when setting up native modules. And Rune allows various levels of integration with the language.

On the simplest level an external type is entirely opaque. Rune knows nothing about it except that it is a value bound to a variable.

Below is the most simple example of an external type. It's implemented by deriving Any which can do a lot of heavy lifting for us.

use rune::runtime::Vm;
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Any, ContextError, Diagnostics, Module};

use std::sync::Arc;

#[derive(Debug, Any)]
struct External {
    value: u32,
}

pub fn module() -> Result<Module, ContextError> {
    let mut module = Module::new();
    module.ty::<External>()?;
    Ok(module)
}

fn main() -> rune::support::Result<()> {
    let m = module()?;

    let mut context = rune_modules::default_context()?;
    context.install(m)?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            pub fn main(external) {
                external
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(runtime, Arc::new(unit));

    let output = vm.call(["main"], (External { value: 42 },))?;
    let output: External = rune::from_value(output)?;
    println!("{:?}", output);
    assert_eq!(output.value, 42);
    Ok(())
}

This type isn't particularly useful. Attempting to access a field on external would simply error. We have to instruct Rune how the field is accessed.

Luckily Any allows us to easily do that by marking the fields we want to make accessible to Rune with #[rune(get)].

#[derive(Debug, Any)]
struct External {
    #[rune(get)]
    value: u32,
}

With our newfound power we can now read external.value.

pub fn main(external) {
    println!("{}", external.value);
}

Setting the value is similarly simple. We simply mark the field with #[rune(set)].

#[derive(Debug, Any)]
struct External {
    #[rune(get, set)]
    value: u32,
}

And now we can both read and write to external.value.

pub fn main(external) {
    external.value = external.value + 1;
}

Note: See the section about Field Functions for a complete reference of the available attributes.

External enums

Enums have a few more tricks that we need to cover. We want to be able to pattern match and construct external enums.

There are three kinds of variants in an enum:

  • Unit variants which have no fields. E.g. External::Unit.
  • Tuple variants which have numerical fields. E.g. External::Tuple(1, 2, 3).
  • Struct variants which have named fields. E.g. External::Struct { a: 1, b: 2, c: 3 }.

Pattern matching is supported out of the box. The only thing to take note of is that pattern matching will only see fields that are annotated with #[rune(get)].

So the following type:

enum External {
    First(#[rune(get)] u32, u32),
    Second(#[rune(get)] u32),
}

Could be pattern matched like this in Rune:

pub fn main(external) {
    match external {
        External::First(a) => a,
        External::Second(b) => b,
    }
}

Take note on how External::First only "sees" the field marked with #[rune(get)].

Let's add a struct variant and see what we can do then:

enum External {
    First(#[rune(get)] u32, u32),
    Second(#[rune(get)] u32),
    Third {
        a: u32,
        b: u32,
        #[rune(get)]
        c: u32,
    },
}

And let's add Third to our example:

pub fn main(external) {
    match external {
        External::First(a) => a,
        External::Second(b) => b,
        External::Third { c } => b,
    }
}

Constructing enum variants

Unit and tuple variants can be annotated with #[rune(constructor)] which is necessary to allow for building enums in Rune. But in order for the constructor to work, all fields must be annotated with #[rune(get)].

enum External {
    #[rune(constructor)]
    First(#[rune(get)] u32, #[rune(get)] u32),
    #[rune(constructor)]
    Second(#[rune(get)] u32),
    Third {
        a: u32,
        b: u32,
        #[rune(get)]
        c: u32,
    },
}
pub fn main() {
    External::First(1, 2)
}

But why do we have the #[rune(get)] requirement? Consider what would happen otherwise. How would we construct an instance of External::First without being able to specify what the values of all fields are? The answer is that all fields must be visible. Alternatively we can declare another constructor as an associated function. The same way we'd do it in Rust.

Try operator

The try operator (?) is a control flow operator which causes a function to return early in case the value being tried over has a certain value.

For Option, this causes the function to return if it has the Option::None variant.

fn checked_div_mod(a, b) {
    let div = a.checked_div(b)?;
    Some((div, a % b))
}

if let Some((div, m)) = checked_div_mod(5, 2) {
    println!("Result: {}, {}", div, m);
}

if let Some((div, m)) = checked_div_mod(5, 0) {
    println!("Result: {}, {}", div, m);
}
$> cargo run -- run scripts/book/try_operator/basic_try.rn
Result: 2, 1

Generators

Generators are a convenient method for constructing functions which are capable of suspending themselves and their state.

The simplest use case for generators is to create a kind of iterator, whose state is stored in the generator function.

With this, we can create a fairly efficient generator to build fibonacci numbers.

fn fib() {
    let a = 0;
    let b = 1;

    loop {
        yield a;
        let c = a + b;
        a = b;
        b = c;
    }
}

let g = fib();

while let Some(n) = g.next() {
    println!("{n}");

    if n > 100 {
        break;
    }
}
$> cargo run -- run scripts/book/generators/fib_generator.rn
0
1
1
2
3
5
8
13
21
34
55
89
144

Advanced generators with GeneratorState

Generators internally are a bit more complex than that. The next function simply slates over some of that complexity to make simple things easier to do.

The first thing to know is that yield itself can actually produce a value, allowing the calling procedure to send values to the generator.

fn printer() {
    let collected = [];

    for _ in 0..2 {
        let out = yield;
        println!("{:?}", out);
        collected.push(out);
    }

    assert_eq!(collected, ["John", (1, 2, 3)]);
}

let printer = printer();
printer.resume(());
printer.resume("John");
printer.resume((1, 2, 3));
$> cargo run -- run scripts/book/generators/send_values.rn
"John"
(1, 2, 3)

But wait, what happened to the first value we sent, 1?

Well, generators don't run immediately, they need to be "warmed up" by calling resume once. At that point it runs the block prior to the first yield, we can see this by instrumenting our code a little.

fn printer() {
    loop {
        println!("waiting for value...");
        let out = yield;
        println!("{out:?}");
    }
}

let printer = printer();

println!("firing off the printer...");
printer.resume(());
println!("ready to go!");

printer.resume("John");
printer.resume((1, 2, 3));
$> cargo run -- run scripts/book/generators/bootup.rn
firing off the printer...
waiting for value...
ready to go!
"John"
waiting for value...
(1, 2, 3)
waiting for value...

Ok, so we understand how to send values into a generator. But how do we receive them?

This adds a bit of complexity, since we need to pull out GeneratorState. This enum has two variants: Yielded and Complete, and represents all the possible states a generator can suspend itself into.

fn print_once() {
    let out = yield 1;
    println!("{:?}", out);
    2
}

let printer = print_once();
dbg!(printer.resume(()));
dbg!(printer.resume("John"));
$> cargo run -- run scripts/book/generators/states.rn
Yielded(1)
"John"
Complete(2)

After the first call to resume, we see that the generator produced Yielded(1). This corresponds to the yield 1 statement in the generator.

The second value we get is Complete(2). This corresponds to the return value of the generator.

Trying to resume the generator after this will cause the virtual machine to error.

fn print_once() {
    yield 1
}

let printer = print_once();
dbg!(printer);
dbg!(printer.resume(()));
dbg!(printer.resume("John"));
dbg!(printer);
dbg!(printer.resume(()));
$> cargo run -- run scripts/book/generators/error.rn
Generator { completed: false }
Yielded(1)
Complete("John")
Generator { completed: true }
error: virtual machine error
   โ”Œโ”€ scripts/book/generators/error.rn:11:9
   โ”‚
11 โ”‚     dbg!(printer.resume(()));
   โ”‚          ^^^^^^^^^^^^^^^^^^ cannot resume a generator that has completed

Closures

We've gone over functions before, and while incredibly useful there's a few more tricks worth mentioning.

We'll also be talking about closures, an anonymous function with the ability to close over its environment, allowing the function to use and manipulate things from its environment.

Function pointers

Every function can be converted into a function pointer simply by referencing its name without calling it.

This allows for some really neat tricks, like passing in a function which represents the operation you want another function to use.

fn do_thing(op) {
    op(1, 2)
}

fn add(a, b) {
    a + b
}

fn sub(a, b) {
    a - b
}

println!("Result: {}", do_thing(add));
println!("Result: {}", do_thing(sub));
$> cargo run -- run scripts/book/closures/function_pointers.rn
Result: 3
Result: -1

Closures

Closures are anonymous functions which closes over their environment. This means that they capture any variables used inside of the closure, allowing them to be used when the function is being called.

fn work(op) {
    op(1, 2)
}

let n = 1;
println!("Result: {}", work(|a, b| n + a + b));
println!("Result: {}", work(|a, b| n + a * b));
$> cargo run -- run scripts/book/closures/basic_closure.rn
Result: 4
Result: 3

Hint: Closures which do not capture their environment are identical in representation to a function.

Functions outside of the Vm

Now things get really interesting. Runestick, the virtual machine driving Rune, has support for passing function pointers out of the virtual machine using the Function type.

This allows you to write code that takes a function constructed in Rune, and use it for something else.

use rune::runtime::Function;
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Vm};

use std::sync::Arc;

fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            fn foo(a, b) {
                a + b
            }

            pub fn main() {
                foo
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(runtime, Arc::new(unit));
    let output = vm.call(["main"], ())?;
    let output: Function = rune::from_value(output)?;

    println!("{}", output.call::<i64>((1, 3)).into_result()?);
    println!("{}", output.call::<i64>((2, 6)).into_result()?);
    Ok(())
}
$> cargo run --example rune_function
4
8

Note that these functions by necessity have to capture their entire context and can take up quite a bit of space if you keep them around while cycling many contexts or units.

Values used in a closure can also be moved into it using the move keyword, guaranteeing that no one else can use it afterwards. An attempt to do so will cause a compile error.

fn work(op) {
    op(1, 2)
}

pub fn main() {
    let n = 1;
    println!("Result: {}", work(move |a, b| n + a + b));
    assert!(!is_readable(n));
}
$> cargo run -- run scripts/book/closures/closure_move.rn.fail
error: compile error
  โ”Œโ”€ scripts/book/closures/closure_move.rn.fail:7:33
  โ”‚
7 โ”‚     println!("Result: {}", work(move |a, b| n + a + b));
  โ”‚                                 --------------------- moved here
8 โ”‚     assert!(!is_readable(n));
  โ”‚                          ^ variable moved

Moving indiscriminately applies to types which in principle could be copied (like integers). We simply don't have the necessary type information available right now to make that decision. If you know that the value can be copied and you want to do so: assign it to a separate variable.

Asynchronous programming

Rune has first class support for Rust-like asynchronous programming. In this section we'll be briefly covering what asynchronous programming is, and how it applies to Rune as a dynamic programming language.

What is it?

Asynchronous code allows us to run multiple tasks concurrently, and work with the result of those tasks.

A typical example would be if we want to perform multiple HTTP requests at once:

let a = http::get("https://google.com");
let b = http::get("https://amazon.com");

loop {
    let res = select {
        res = a => res?,
        res = b => res?,
    };

    match res {
        () => break,
        result => {
            println!("{}", result.status());
        }
    }
}
$> cargo run -- run scripts/book/async/async_http.rn
200 OK
200 OK

In the above code we send two requests concurrently. They are both processed at the same time and we collect the result.

select blocks

A fundamental construct of async programming in Rune is the select block. It enables us to wait on a set of futures at the same time.

A simple example of this is if we were to implement a simple request with a timeout:

struct Timeout;

async fn request(timeout) {
    let request = http::get(`http://httpstat.us/200?sleep=${timeout}`);
    let timeout = time::sleep(time::Duration::from_secs(2));

    let result = select {
        _ = timeout => Err(Timeout),
        res = request => res,
    }?;

    println!("{}", result.status());
    Ok(())
}

if let Err(Timeout) = request(1000).await {
    println!("Request timed out!");
}

if let Err(Timeout) = request(4000).await {
    println!("Request timed out!");
}
$> cargo run -- run scripts/book/async/async_http_timeout.rn
200 OK
Request timed out!

But wait, this is taking three seconds. We're not running the requests concurrently any longer!

Well, while the request and the timeout is run concurrently, the request function is run one at-a-time.

To fix this we need two new things: async functions and .await.

async functions

async functions are just like regular functions, except that when called they produce a Future.

In order to get the result of this Future it must be .awaited. And .await is only permitted inside of async functions and closures.

use std::future;

struct Timeout;

async fn request(timeout) {
    let request = http::get(`http://httpstat.us/200?sleep=${timeout}`);
    let timeout = time::sleep(time::Duration::from_secs(2));

    let result = select {
        _ = timeout => Err(Timeout),
        res = request => res,
    }?;

    Ok(result)
}

for result in future::join([request(1000), request(4000)]).await {
    match result {
        Ok(result) => println!("Result: {}", result.status()),
        Err(Timeout) => println!("Request timed out!"),
    }
}
$> cargo run -- run scripts/book/async/async_http_concurrent.rn
Result: 200 OK
Request timed out!

async closures

Closures can be prefixed with the async keyword, meaning calling them will produce a future.

fn do_request(url) {
    async || {
        Ok(http::get(url).await?.status())
    }
}

let future = do_request("https://google.com");
let status = future().await?;
println!("Status: {status}");
$> cargo run -- run scripts/book/async/async_closure.rn
Status: 200 OK

async blocks

Blocks can be marked with async to produce on-the-fly futures. These blocks can capture variables the same way as closures do, but take no arguments.

fn do_request(url) {
    async {
        Ok(http::get(url).await?.status())
    }
}

let future = do_request("https://google.com");
let status = future.await?;
println!("Status: {status}");
$> cargo run -- run scripts/book/async/async_blocks.rn
Status: 200 OK

Streams

Streams are the asynchronous version of Generators.

They have almost identical next and resume functions, but each must be used with .await, and we are now allowed to use asynchronous functions inside of the generator.

async fn send_requests(list) {
    let client = http::Client::new();

    let do_request = async |url| {
        Ok(client.get(url).send().await?.status())
    };

    for url in list {
        yield do_request(url).await;
    }
}

let requests = send_requests(["https://google.com", "https://amazon.com"]);

while let Some(status) = requests.next().await.transpose()? {
    println!("{}", status);
}
$> cargo run -- run scripts/book/streams/basic_stream.rn
200 OK
200 OK

Multithreading

Rune is thread safe, but the Vm does not implement Sync so cannot directly be shared across threads. This section details instead how you are intended to use Rune in a multithreaded environment.

Compiling a Unit and a RuntimeContext are expensive operations compared to the cost of calling a function. So you should try to do this as little as possible. It is appropriate to recompile a script when the source of the script changes. See the Hot reloading section for more information on this.

Once you have a Unit and a RuntimeContext they are thread safe and can be used by multiple threads simultaneously through Arc<Unit> and Arc<RuntimeContext>. Constructing a Vm with these through Vm::new is a very cheap operation.

#![allow(unused)]
fn main() {
let unit: Arc<Unit> = /* todo */;
let context: Arc<RuntimeContext> = /* todo */;

std::thread::spawn(move || {
    let mut vm = Vm::new(unit, context);
    let value = vm.call(["function"], (42,))?;
    Ok(())
});
}

Virtual machines do allocate memory. To avoide this overhead you'd have to employ more advanced techniques, such as storing virtual machines in a pool or thread locals. Once a machine has been acquired the Unit and RuntimeContext associated with it can be swapped out to the ones you need using Vm::unit_mut and Vm::context_mut respectively.

Using Vm::send_execute is a way to assert that a given execution is thread safe. And allows you to use Rune in asynchronous multithreaded environments, such as Tokio. This is achieved by ensuring that all captured arguments are ConstValue's, which in contrast to Value's are guaranteed to be thread-safe:

use rune::alloc::prelude::*;
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Vm};

use std::sync::Arc;

#[tokio::main]
async fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;
    let runtime = Arc::new(context.runtime()?);

    let mut sources = rune::sources! {
        entry => {
            async fn main(timeout) {
                time::sleep(time::Duration::from_secs(timeout)).await
            }
        }
    };

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let vm = Vm::new(runtime, Arc::new(unit));

    let execution = vm.try_clone()?.send_execute(["main"], (5u32,))?;
    let t1 = tokio::spawn(async move {
        execution.async_complete().await.unwrap();
        println!("timer ticked");
    });

    let execution = vm.try_clone()?.send_execute(["main"], (2u32,))?;
    let t2 = tokio::spawn(async move {
        execution.async_complete().await.unwrap();
        println!("timer ticked");
    });

    tokio::try_join!(t1, t2).unwrap();
    Ok(())
}

Finally Function::into_sync exists to coerce a function into a SyncFunction, which is a thread-safe variant of a regular Function. This is a fallible operation since all values which are captured in the function-type in case its a closure has to be coerced to ConstValue. If this is not the case, the conversion will fail.

Hot reloading

Compiling a Unit and a [RuntimeContext] are expensive operations compared to the cost of calling a function. So you should try to do this as little as possible. It is appropriate to recompile a script when the source of the script changes. This section provides you with details for how this can be done when loading scripts from the filesystem.

A typical way to accomplish this is to watch a scripts directory using the notify crate. This allow the application to generate events whenever changes to the directory are detected. See the hot_reloading example and in particular the PathReloader type.

#[path = "hot_reloading/path_reloader.rs"]
mod path_reloader;

use std::path::PathBuf;
use std::pin::pin;
use std::sync::Arc;

use anyhow::{Context as _, Result};
use rune::{Context, Vm};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
    let root =
        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").context("missing CARGO_MANIFEST_DIR")?);

    let context = Context::with_default_modules()?;

    let mut exit = pin!(tokio::signal::ctrl_c());
    let mut reloader = pin!(path_reloader::PathReloader::new(
        root.join("scripts"),
        &context
    )?);

    let context = Arc::new(context.runtime()?);

    let mut events = Vec::new();

    loop {
        tokio::select! {
            _ = exit.as_mut() => {
                break;
            }
            result = reloader.as_mut().watch(&mut events) => {
                result?;
            }
        }

        for event in events.drain(..) {
            let mut vm = Vm::new(context.clone(), event.unit);

            match event.kind {
                path_reloader::EventKind::Added => {
                    if let Err(error) = vm.call(["hello"], ()) {
                        println!("Error: {}", error);
                    }
                }
                path_reloader::EventKind::Removed => {
                    if let Err(error) = vm.call(["goodbye"], ()) {
                        println!("Error: {}", error);
                    }
                }
            }
        }
    }

    Ok(())
}

Macros

Rune has support for macros. These are functions which expand into code, and can be used by library writers to "extend the compiler".

For now, the following type of macros are support:

  • Function-like macros expanding to items (functions, type declarations, ..).
  • Function-like macros expanding to expression (statements, blocks, async blocks, ..).
  • Attribute macros expanding around a function.

Macros can currently only be defined natively. This is to get around the rather tricky issue that the code of a macro has to be runnable during compilation. Native modules have an edge here, because they have to be defined at a time when they are definitely available to the compiler.

Don't worry though, we will be playing around with macro fn as well, but at a later stage ๐Ÿ˜‰ (See issue #27).

Native modules also means we can re-use all the existing compiler infrastructure for Rune as a library for macro authors. Which is really nice!

Writing a native macro

The following is the definition of the stringy_math! macro. Which is a macro that can be invoked on expressions.

This relies heavily on a Rune-specific quote! macro. Which is inspired by its famed counterpart in the Rust world. A major difference with Rune quote! is that we need to pass in the MacroContext when invoking it. This is a detail which will be covered in one of the advanced sections.

use crate as rune;
use crate::ast;
use crate::compile;
use crate::macros::{quote, MacroContext, TokenStream};
use crate::parse::Parser;

/// Implementation of the `stringy_math!` macro.
#[rune::macro_]
pub fn stringy_math(
    cx: &mut MacroContext<'_, '_, '_>,
    stream: &TokenStream,
) -> compile::Result<TokenStream> {
    let mut parser = Parser::from_token_stream(stream, cx.input_span());

    let mut output = quote!(0);

    while !parser.is_eof()? {
        let op = parser.parse::<ast::Ident>()?;
        let arg = parser.parse::<ast::Expr>()?;

        output = match cx.resolve(op)? {
            "add" => quote!((#output) + #arg),
            "sub" => quote!((#output) - #arg),
            "div" => quote!((#output) / #arg),
            "mul" => quote!((#output) * #arg),
            _ => return Err(compile::Error::msg(op, "unsupported operation")),
        }
    }

    parser.eof()?;
    Ok(output.into_token_stream(cx)?)
}

A macro is added to a Module using the Module::macro_ function.

pub fn module() -> Result<rune::Module, rune::ContextError> {
    let mut module = rune::Module::new(["test", "macros"]);
    module.macro_meta(stringy_math)?;
    Ok(module)
}

With this module installed, we can now take stringy_math! for a spin.

use ::test::macros::stringy_math;

pub fn main() {
    stringy_math!(add 10 sub 2 div 3 mul 100)
}

Running this would return 200.

Advanced

This chapter is dedicated to the advanced topics of Rune. Here we will discuss the advanced and internal details of the language. This chapter is primarily targeted at people who want to understand Rune and Runestick in detail.

Drop order

Rune implements the following rules when determining when a place should be dropped.

Places are distinct from values, in that they refer to a place where a value is stored, not the value itself.

The distinction becomes apparent when we note that the same value can be referenced by multiple places:

let a = 42;
let b = a;

Above, b is a distinct place that refers to the same value as a.

There are two ways for a value to be dropped:

  • All the places referencing go out of scope.
  • The value is explicitly dropped with drop(value).

The second variant causes the value to be dropped. Using any of the places referencing that value after it has been dropped will cause an error.

Variables

A variable declaration like this:

let var = 42;

Defines a place called var.

Once variables like these go out of scope, their place is dropped. However, dropping a place doesn't necessarily mean the value is dropped. This only happens when that is the last place referencing that variable.

let object = {
    let var = 42;
    let object = #{ var };
};

// object can be used here and `var` is still live.

Temporaries

Temporaries are constructed when evaluating any non-trivial expression, such as this:

let var = [42, (), "hello"];

The drop order for temporaries is not strictly defined and can be extended. But never beyond the block in which they are defined.

Safety

Rune is implemented in Rust, but that doesn't automatically make the language safe (as Rust defines safety) since there are some uses of unsafe. In this section we'll be documenting the pieces of the implementation which are currently unsafe, rationalize, and document potential soundness holes.

Internal Any type

Rune uses an internal Any type.

Apart from the hash conflict documented above, the implementation should be sound. We have an internal Any type instead of relying on Box<dyn Any> to allow AnyObjVtable to be implementable by external types to support external types through a C ffi.

Shared<T> and UnsafeToRef / UnsafeToMut

A large chunk of the Shared<T> container is unsafe. This is a container which is behaviorally equivalent to Rc<RefCell<T>>.

We have this because it merges Rc and RefCell and provides the ability to have "owned borrows" and the ability to unsafely decompose these into a raw pointer and a raw guard, which is used in many implementations of UnsafeToRef or UnsafeToMut.

UnsafeToRef and UnsafeToMut are conversion traits which are strictly used internally to convert values into references. Its safety is documented in the trait.

Sandboxing

Rune is capable of enforcing the following types of limitations:

  • Memory limiting, where you specify the maxium amount of memory that Rune may use either during compilation or execution.
  • Instruction budgeting, where you can specify how many instructions the virtual machine is permitted to execute.

Instruction budgeting

Instruction budgeting is performed using the with function in the rune::budget module.

The with function is capable of wrapping functions and futures. When wrapping a future it ensures that the budget is suspended appropriately with the execution of the future.

Budgeting is only performed on a per-instruction basis in the virtual machine. What exactly constitutes an instruction might be a bit vague. But important to note is that without explicit co-operation from native functions the budget cannot be enforced. So care must be taken with the native functions that you provide to Rune to ensure that the limits you impose cannot be circumvented.

Memory limiting

Memory limiting is performed using the with function in the rune::alloc::limit module.

#![allow(unused)]
fn main() {
use rune::alloc::prelude::*;
use rune::alloc::limit;

let f = limit::with(1024, || {
    let mut vec = Vec::<u32>::try_with_capacity(256)?;

    for n in 0..256u32 {
        vec.try_push(n)?;
    }

    Ok::<_, rune_alloc::Error>(vec.into_iter().sum::<u32>())
});

let sum = f.call()?;
assert_eq!(sum, 32640);
}

In order for memory limiting to work as intended, you're may only use the collections provided in the rune::alloc module. These contain forks of popular collections such as std::collections and hashbrown.

The with function is capable of wrapping functions and [futures]. When wrapping a future it ensures that the limit is suspended appropriately with the execution of the future.

The stack

Runestick is a stack-based virtual machine. It has two primary places where things are stored. The stack and the heap. It has no registers.

Instructions in the virtual machine operate off the stack. Let's take a look at the add operation with --trace and --dump-stack.

1 + 3
$> cargo run -- run scripts/book/the_stack/add.rn --trace --dump-stack
fn main() (0xe7fc1d6083100dcd):
  0000 = integer 1
    0+0 = 1
  0001 = integer 3
    0+0 = 1
    0+1 = 3
  0002 = add
    0+0 = 4
  0003 = return
    *empty*
== 4 (7.7691ms)
# stack dump after halting
frame #0 (+0)
    *empty*

Let's examine the stack after each instruction.

  0000 = integer 1
    0+0 = 1

We evaluate the integer 1 instruction, which pushes an integer with the value 1 onto the stack.

  0001 = integer 3
    0+0 = 1
    0+1 = 3

We evaluate the integer 3 instruction, which pushes an integer with the value 3 onto the stack.

  0002 = add
    0+0 = 4

We evaluate the add instruction which pops two values from the stack and adds them together. Two integers in this instance would use built-in accelerated implementations which performs addition.

  0003 = return
== 4 (7.7691ms)

We return from the virtual machine. The last value of the stack will be popped as the return value.

# stack dump
frame #0 (+0)

This is the stack dump we see after the virtual machine has exited. It tells us that at call frame #0 (+0), the last and empty call frame at stack position +0 there is nothing on the stack.

Call frames

Call frames are a cheap isolation mechanism available in the virtual machine. They define a subslice in the stack, preventing the vm from accessing values that are outside of the slice.

They have the following rules:

  • Instructions cannot access values outside of their current call frame.
  • When we return from the call frame the subslice must be empty.

If any these two conditions aren't maintained, the virtual machine will error.

Call frames fill two purposes. The subslice provides a well-defined variable region. Stack-relative operations like copy 0 are always defined relative to the top of their call frame. Where copy 0 would mean "copy from offset 0 of the current stack frame".

They also provide a cheap security mechanism against miscompilations. This might be made optional in the future once Rune is more stable, but for now it's helpful to detect errors early and protect the user against bad instructions. But don't mistake it for perfect security. Like stack protection which is common in modern operating systems, the mechanism can be circumvented by malicious code.

To look close at the mechanism, let's trace the following program:

fn foo(a, b) {
    a + b
}

let a = 3;
foo(1, 2) + a
$> cargo run -- run scripts/book/the_stack/call_and_add.rn --trace --dump-stack
fn main() (0xe7fc1d6083100dcd):
  0005 = integer 3
    0+0 = 3
  0006 = integer 1
    0+0 = 3
    0+1 = 1
  0007 = integer 2
    0+0 = 3
    0+1 = 1
    0+2 = 2
  0008 = call 0xbfd58656ec9a8ebe, 2 // fn `foo`
=> frame 1 (1):
    1+0 = 1
    1+1 = 2
fn foo(arg, arg) (0xbfd58656ec9a8ebe):
  0000 = copy 0 // var `a`
    1+0 = 1
    1+1 = 2
    1+2 = 1
  0001 = copy 1 // var `b`
    1+0 = 1
    1+1 = 2
    1+2 = 1
    1+3 = 2
  0002 = add
    1+0 = 1
    1+1 = 2
    1+2 = 3
  0003 = clean 2
    1+0 = 3
  0004 = return
<= frame 0 (0):
    0+0 = 3
    0+1 = 3
  0009 = copy 0 // var `a`
    0+0 = 3
    0+1 = 3
    0+2 = 3
  0010 = add
    0+0 = 3
    0+1 = 6
  0011 = clean 1
    0+0 = 6
  0012 = return
    *empty*
== 6 (45.8613ms)
# full stack dump after halting
  frame #0 (+0)
    *empty*

We're not going to go through each instruction step-by-step like in the last section. Instead we will only examine the parts related to call frames.

We have a call 0xbfd58656ec9a8ebe, 2 instruction, which tells the virtual machine to jump to the function corresponding to the type hash 0xbfd58656ec9a8ebe, and isolate the top two values on the stack in the next call frame.

We can see that the first argument a is in the lowest position, and the second argument b is on the highest position. Let's examine the effects this function call has on the stack.

    0+0 = 3
    0+1 = 1
    0+2 = 2
  0008 = call 0xbfd58656ec9a8ebe, 2 // fn `foo`
=> frame 1 (1):
    1+0 = 1
    1+1 = 2

Here we can see a new call frame frame 1 being allocated, and that it contains two items: 1 and 2.

We can also see that the items are offset from position 1, which is the base of the current call frame. This is shown as the addresses 1+0 and 1+1. The value 3 at 0+0 is no longer visible, because it is outside of the current call frame.

Let's have a look at what happens when we return:

    1+0 = 1
    1+1 = 2
    1+2 = 3
  0003 = clean 2
    1+0 = 3
  0004 = return
<= frame 0 (0):
    0+0 = 3
    0+1 = 3

We call the clean 2 instruction, which tells the vm to preserve the top of the stack (1+2), and clean two items below it, leaving us with 3. We then return, which jumps us back to frame 0, which now has 0+0 visible and our return value at 0+1.

Compiler guide

This is intended to be a guide into the compiler architecture for Rune for people who want to hack on it.

Rune is in heavy development and this section is likely to change a lot.

Compiling a rune program involves the following stages:

  • Queue the initial source files specified by Source::insert.
  • Indexing and macro expansion, which processes tasks in the Worker queue until it is empty. These are:
    • Task::LoadFile - Loads a single source into AST file and indexes it.
    • Task::ExpandUnitWildcard - A deferred expansion of a wildcard import. This must happen after indexing because macros might expand into imports.
  • Compilation which processes a queue of items to be compiled and assembled.

Indexing

Indexing is primarily handled through the Index trait, which are implemented for the type being indexed with the helper of the Indexer.

This walks through the AST to be indexed and construct components into an item path for every:

  • Functions, which adds components named after the function. fn foo would add foo.
  • Closures, blocks, and nested functions, which adds an id component, like $10 where the number depends on how many sibling components there are. These are effectively anonymous, and can't be referenced through the language directly.

Compilation

The compilation stage processed the entire AST of every function that is queued to be compiled and generates a sequence of instructions for them through implementations of the Assemble trait.

This stage uses the Query system to look up metadata about external items, and any external item queried for is subsequently queued up to be built.

Consider the following unit:

return foo();

fn foo() {
    2
}

fn bar() {
    3
}

Let's dump all dynamic functions in it:

$> cargo run -- run scripts/book/compiler_guide/dead_code.rn --dump-functions --warnings
# dynamic functions
0x0 = {root}()
0x481411c4bd0a5f6 = foo()
---
== 2 (59.8ยตs)

As you can see, the code for main::$0::bar was never generated. This is because it's a local function that is never called. And therefore never queried for. So it's never queued to be built in the compilation stage.

State during compilation

Each item in the AST is relatively isolated while they are being compiled. This is one of the benefits of compiling for a stack-based virtual machine - the compilation stage is relatively simple and most reasoning about what instructions to emit can be made locally.

Note that this quickly changes if you want to perform most forms of optimizations. But it's definitely true for naive (and therefore fast!) code generation.

While compiling we keep track of the following state in the Compiler

The source file and id that we are compiling for and global storage used for macro-generated identifiers and literals. This is used to resolve values from the AST through the corresponding Resolve implementation. An example of this is the Resolve implementation of LitStr.

We keep track of local variables using Scopes. Each block creates a new scope of local variables, and this is simply a number that is incremented each time a variable is allocated. These can either be named or anonymous. Each named variable is associated with an offset relative to the current call frame that can be looked up when a variable needs to be used.

We maintain information on loops we're through Loops. This is a stack that contains every loop we are nested in, information on the label in which the loop terminates, and locals that would have to be cleaned up in case we encounter a break expression.

There are a couple more traits which are interesting during compilation:

  • AssembleConst - used for assembling constants.
  • AssembleFn - used for assembling the content of functions.
  • AssembleClosure - used for assembling closures.

Let's look closer at how closures are assembled through AssembleClosure. Once a closure is queried for, it is queued up to be built by the query system. The closure procedure would be compiled and inserted into the unit separately at a given item (like main::$0::$0). And when we invoke the closure, we assemble a call to this procedure.

We can see this call by dumping all the dynamic functions in the following script:

let callable = || 42;
dbg!(callable());
$> cargo run -- run scripts/book/compiler_guide/closures.rn --emit-instructions --dump-functions
# instructions
fn main() (0x1c69d5964e831fc1):
  0000 = load-fn hash=0xbef6d5f6276cd45e // closure `3`
  0001 = copy offset=0 // var `callable`
  0002 = call-fn args=0
  0003 = pop
  0004 = pop
  0005 = return-unit

fn main::$0::$0() (0xbef6d5f6276cd45e):
  0006 = push value=42
  0007 = return address=top, clean=0
# dynamic functions
0xbef6d5f6276cd45e = main::$0::$0()
0x1c69d5964e831fc1 = main()

A function pointer is pushed on the stack load-fn 0xca35663d3c51a903, then copied and called with zero arguments.

Deprecated

These are section of the book which have been deprecated for one reason or another, but we still want to provide links to.

You're probably looking for the section about Template literals.