Native Types

Native types let you expose custom Rust-backed types to scripts. The type checker sees their methods and return types; scripts receive them as opaque values.

Defining a native type🔗

use dogma_vm::{TypeSpec, MethodSpec, Value};

let counter_type = TypeSpec::new("Counter")
    .with_method(
        MethodSpec::new("value", 0, |args| {
            let map = match &args[0] { Value::Map(m) => m, _ => return Err("bad self".into()) };
            let n = map.iter()
                .find(|(k, _)| k == &Value::String("n".into()))
                .and_then(|(_, v)| if let Value::I64(n) = v { Some(*n) } else { None })
                .unwrap_or(0);
            Ok(Value::I64(n))
        })
        .returns("i64"),
    )
    .with_method(
        MethodSpec::new("incremented", 0, |args| {
            let map = match &args[0] { Value::Map(m) => m, _ => return Err("bad self".into()) };
            let n = map.iter()
                .find(|(k, _)| k == &Value::String("n".into()))
                .and_then(|(_, v)| if let Value::I64(n) = v { Some(*n) } else { None })
                .unwrap_or(0);
            Ok(Value::Map(vec![
                (Value::String("__type__".into()), Value::String("Counter".into())),
                (Value::String("n".into()), Value::I64(n + 1)),
            ]))
        })
        .returns("Counter"),
    );

Registering the type🔗

Engine::builder()
    .with_native_type(counter_type)
    .build()

Passing native values to scripts🔗

Native type instances are Value::Map values with a __type__ key identifying the type name. Create them from a capability function:

NativeSpec::new("make_counter", 1, |args| {
    let start = match &args[0] { Value::I64(n) => *n, _ => return Err("expected i64".into()) };
    Ok(Value::Map(vec![
        (Value::String("__type__".into()), Value::String("Counter".into())),
        (Value::String("n".into()), Value::I64(start)),
    ]))
})
.returns("Counter")

In scripts:

import { make_counter } from "myapp"

fn main() {
    let c = make_counter(10)
    let c2 = c.incremented()
    println(c2.value())   // 11
}

The .returns() requirement🔗

Always call .returns("TypeName") on every MethodSpec. The type checker and LSP use this for inference and completions. Without it, return types are unknown and type errors may be missed.

MethodSpec::new("value", 0, handler).returns("i64")   // required