Crate mica

source ·
Expand description

Mica is an embeddable scripting language for Rust. Its goals are:

  • Human-friendly syntax inspired by Ruby and Lua
  • Simple and familiar to Rust developers feature-wise
  • Easily embeddable into existing programs
  • Better performance than most existing Rust scripting languages
## Hello, Mica!

struct Counter impl
    func new(start, increment) constructor = do
        @value = start
        @increment = increment
    end

    func value() = @value

    func increment() = do
        @value = @value + @increment
    end
end

c = Counter.new(1, 1)
while c.value < 100 do
    print(c.value)
    if c.value.mod(2) == 0 do
        print("even!")
    end
    c.increment()
end

Getting started

Seeing you’re here, you probably want to embed Mica into your own program. Worry not! There’s an easy-to-use, high-level API just waiting to be discovered by adventurous people like you.

To start out, build an Engine:

use mica::Engine;

let mut engine = Engine::new();

Running code

Before we can run code, we must compile it into a Script. Note that compiling a script mutably borrows your engine, and it cannot be used for anything else while a compiled script exists. This is because a script must only ever be run in the engine it was compiled in. Thus, the script mutably borrows the engine it was compiled by, so that you don’t have to worry about confusing two engines.

// The first argument passed is the file name, which is used in error messages.
let mut script = engine.compile("hello.mi", r#" print("Hello, world!") "#)?;

Now that you have a script, you can begin executing it by calling script.start(). This will start up a new Fiber, which represents a pausable thread of execution.
Though currently there’s no way to signal that you want to pause execution from within Mica.

let mut fiber = script.start();

A fiber doesn’t start running immediately though. To make it start interpreting your bytecode, call resume:

use mica::Value;
while let Some(value) = fiber.resume::<Value>()? {
    println!("{value:?}");
}
// Output:
// Hello, world!
// nil

If you only care about the final value returned by the fiber, you can call trampoline instead. The name comes from the fact that the function bounces into and out of the VM until execution is done.

Note that this function discards all intermediary values returned by the VM.

let result: Value = fiber.trampoline()?;
println!("{result:?}");

Working with values

As shown above, running a script produces one or more values. This is an important feature of Mica – it’s an expression-oriented language. Everything you could possibly think of produces a value.

To make working with values less annoying, types that can be obtained from Mica’s dynamically-typed values implement the TryFromValue trait, and each function returning values from the VM conveniently converts the returned value to a user-specified type.

// Tip: A fiber can be started immediately after compiling a script, by using Engine::start.
let mut fiber = engine.start("arithmetic.mi", r#" 2 + 2 * 2 "#)?;
let result: f64 = fiber.trampoline()?;
assert_eq!(result, 6.0);

Evaluating arbitrary code is fun, but it’s not very useful unless we can give it some inputs from our program. For that, we can use globals. Globals can be set using Engine::set, and retrieved using Engine::get.

engine.set("x", 1.0f64);
engine.set("y", 2.0f64);
let result: f64 = engine.start("globals.mi", r#" x + y * 10 "#)?.trampoline()?;
assert_eq!(result, 21.0);

Scripts can also set globals, but this functionality is planned to be removed at some point in favor of just returning values.

// The unit type implements TryFromValue that expects a nil value.
let _: () =
    engine.start(
        "code.mi",
        r#" x = 1
            nil "# // Explicitly return nil, because every assignment evaluates to its right-hand side
    )?
    .trampoline()?;
let x: f64 = engine.get("x")?;
assert_eq!(x, 1.0);

Calling Rust from Mica

Mica wouldn’t be an embeddable scripting language worth your time if it didn’t have a way of calling Rust functions. To register a Rust function in the VM, Engine::add_function can be used:

engine.add_function("double", |x: f64| x * x)?;
assert_eq!(
    engine.start("ffi.mi", "double(2)")?.trampoline::<f64>()?,
    4.0
);

In the Mica VM Rust functions are named “foreign”, because they are foreign to the VM. This nomenclature is also used in this crate. Apart from this, Rust functions that are registered into the global scope, and do not have a self parameter, are called “bare” in the documentation.

Rust functions registered in the VM can also be fallible, and return a Result<T, E>.

// Note that the API also understands numeric types other than `f64`. However, the only type of
// numbers Mica supports natively is `f64`, so converting from bigger types incurs a precision loss.
engine.add_function("parse_integer_with_radix", |s: String, radix: u32| {
    usize::from_str_radix(&s, radix)
})?;
assert_eq!(
    engine.start("ffi.mi", r#" parse_integer_with_radix("FF", 16) "#)?.trampoline::<f64>()?,
    255.0
);

Calling a Rust function with the incorrect number of arguments, or incorrect argument types, will raise a runtime error in the VM.

assert!(engine.start("ffi.mi", r#" parse_integer_with_radix()         "#)?.trampoline::<f64>().is_err());
assert!(engine.start("ffi.mi", r#" parse_integer_with_radix(1, 16)    "#)?.trampoline::<f64>().is_err());
assert!(engine.start("ffi.mi", r#" parse_integer_with_radix("aa", 16) "#)?.trampoline::<f64>().is_ok());

Due to limitations of Rust’s type system, strongly typed functions like the one in the example above can only have up to 8 arguments. If more arguments, or a variable amount is needed, a function can accept Arguments as its sole argument, and use it to process arguments.

Do note however that Arguments’ API is comparatively low-level and will have you working with RawValues that are unsafe in many ways. If you really need that many arguments, maybe it’s time to rethink your APIs.

Rust types in Mica

Mica allows for registering arbitrary user-defined types in the VM. As an example, let’s implement the Counter type from the example code at the top of the page, but in Rust.

First of all, for Rust type system reasons, your type needs to implement UserData.

use mica::UserData;

struct Counter {
    value: usize,
    increment: usize,
}

impl UserData for Counter {}

With a type set up, you can then create a TypeBuilder and use it in Engine::add_type.

use mica::TypeBuilder;

impl Counter {
    fn value(&self) -> usize { self.value }
    fn increment(&mut self) {
        self.value += self.increment;
    }
}

engine.add_type(
    // The argument passed to TypeBuilder::new is the name of the global that we want to bind the
    // type under.
    TypeBuilder::<Counter>::new("Counter")
        .add_static("new", |value, increment| Counter { value, increment })
        .add_function("value", Counter::value)
        .add_function("increment", Counter::increment)
);

assert_eq!(
    engine
        .start("type.mi", r#"
            counter = Counter.new(10, 2)
            counter.increment()
            counter.value
        "#)?
        .trampoline::<f64>()?,
    12.0,
);

Unfortunately Rust’s orphan rules prevent traits from being implemented on types from other crates, so the only way of binding someone else’s type is by creating a newtype struct and implementing UserData on it.

struct File(std::fs::File);

impl UserData for File {}

Calling Mica from Rust

It’s also possible to call functions from the Mica VM in Rust. For that, the Engine::call function may be used.

let get_greeting =
    engine
        .start(
            "function.mi",
            r#"
                (func (x) = "Hello, ".cat(x).cat("!"))
            "#
        )?
        .trampoline()?;
let greeting: String = engine.call(get_greeting, [Value::new("world")])?;
assert_eq!(greeting, "Hello, world!");

Apart from bare functions, it’s also possible to call methods, by using Engine::call_method.

let greeter_type =
    engine
        .start(
            "greeter.mi",
            r#"
                struct Greeter impl
                    func new(template) constructor = do
                        @template = template
                    end

                    func greetings(for_whom) =
                        @template.replace("{target}", for_whom)
                end
            "#
        )?
        .trampoline()?;
let greeter = engine.call_method(greeter_type, ("new", 1), [Value::new("Hello, {target}!")])?;
let greeting: String = engine.call_method(greeter, ("greetings", 1), [Value::new("world")])?;
assert_eq!(greeting, "Hello, world!");

Defining traits

Usually you don’t want to call “bare” methods, because by convention they’re meant to be used within the script itself. If you want to let types implement an interface, you can do so with traits, which in addition to scripts, can also be defined using the Rust API.

The advantage of using the Rust API directly is that calling trait methods becomes less verbose and requires less indirections, which improves performance.

let mut builder = engine.build_trait("RandomNumberGenerator")?;
// NB: You *must not* reuse m_generate across different Engines.
let m_generate = builder.add_function("generate", 0)?;
let random_number_generator = builder.build();
// Note that unlike with types, you have to set the trait explicitly.
// This is a quirk of how the borrow hierarchy works currently.
engine.set("RandomNumberGenerator", random_number_generator)?;

// Once the trait is exposed to scripts, we can now define types that implement it.
let rng: Value =
    engine
        .start(
            "rng.mi",
            r#"
                struct Generator impl
                    func new() constructor = nil

                    as RandomNumberGenerator
                        # https://xkcd.com/221/
                        func generate() = 4
                    end
                end
                .new()  # Note that trait methods can be called on instances only.
            "#
        )?
        .trampoline()?;
let number: u32 = engine.call_method(rng, m_generate, [])?;
assert_eq!(number, 4);

Re-exports

pub use crate::ll::gc::Gc;

Modules

Types for representing implementations of built-in traits.
The Mica core library. Provides the fundamental set of functions and types.
Variants of ForeignFunction.
Helper types for IntoValue.
The low-level (ll) implementation of the Mica programming language.

Structs

Arguments passed to a varargs function.
Options for debugging the language implementation.
Start here! An execution engine. Contains information about things like globals, registered types, etc.
A fiber represents an independent, pausable thread of code execution.
An ID unique to an engine, identifying a global variable.
An ID unique to an engine, identifying a method signature.
The number of parameters in a method.
Wrapper struct for marking functions that use the with-raw-self calling convention.
A script pre-compiled into bytecode.
Allows you to build traits programatically from Rust code.
A builder that allows for binding APIs with user-defined types.

Enums

An error.
The number of parameters in a bare function.
The kind of a raw function. The kind of the function (bytecode or FFI).
A dynamically typed value.

Traits

Definitions of basic types provided by a core library. This role is usually fulfilled by corelib.
A Rust function that can be called from Mica.
A trait for names convertible to global IDs.
Trait implemented by all types that can be converted into Values and may require an engine to do so.
Implemented by every type that can be used as a method signature.
Extensions for converting Results into a mica-language FFI-friendly structure.
Extensions for converting Results into a Mica FFI-friendly structure.
Implemented by all types that can be a &mut self parameter in an instance function.
A trait for names convertible to global IDs.
Implemented by all types that can be a &self parameter in an instance function.
Implemented by types that can be constructed as owned from Values.
Marker trait for all user data types.

Type Definitions

A raw ll error, with metadata such as stack traces.
A raw ll error kind.
The implementation of a raw foreign function. The signature of a raw foreign function.