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