This document contains a collection of examples of how to interact with Koto from Rust code.
The complete API documentation can be found here.
To run a Koto script, instantiate koto::Koto and call compile_and_run:
use koto::{Result, prelude::*};
fn main() -> Result<()> {
let script = "print 'Hello, World!'";
Koto::with_settings(KotoSettings::default().inherit_io()).compile_and_run(script)?;
Ok(())
}
The result of calling compile_and_run is a KValue, which is Koto's main
value type.
KValue is an enum that contains variants for each of the core Koto types,
like Number, String, etc.
The type of a KValue as a string can be retrieved via KValue::type_as_string,
and to render a KValue, call Koto::value_to_string.
use anyhow::{Result, bail};
use koto::prelude::*;
fn main() -> Result<()> {
let script = "1 + 2";
let mut koto = Koto::default();
match koto.compile_and_run(script)? {
KValue::Number(result) => {
println!("The result of '{script}' is {result}");
}
other => bail!(
"Expected a Number, found '{}': ({})",
other.type_as_string(),
koto.value_to_string(other)?
),
}
Ok(())
}
Values that are exported from the script are inserted in to the exports map,
which can be accessed by calling Koto::exports().
use anyhow::{Result, bail};
use koto::prelude::*;
fn main() -> Result<()> {
let script = "
export
number: 42
string: 'Hello from Koto'
";
let mut koto = Koto::default();
koto.compile_and_run(script)?;
let exports = koto.exports();
let Some(KValue::Number(exported_number)) = exports.get("number") else {
bail!("Expected an exported number");
};
let Some(KValue::Str(exported_string)) = exports.get("string") else {
bail!("Expected an exported string");
};
println!("Exported number: {exported_number}");
println!("Exported string: '{exported_string}'");
Ok(())
}
Types that implement serde::Deserialize and Serialize can be converted
to and from Koto values via koto::serde::to_koto_value and from_koto_value.
use koto::{
Result,
prelude::*,
serde::{from_koto_value, to_koto_value},
};
use serde::{Deserialize, Serialize};
fn main() -> Result<()> {
let script = "
match request
'one_to_four' then
caption = 'one to four'
numbers = 1, 2, 3, 4
'five_to_eight' then
caption = 'five to eight'
numbers = 5, 6, 7, 8
export {caption, numbers}
";
let mut koto = Koto::default();
koto.prelude()
.insert("request", to_koto_value(Request::FiveToEight)?);
koto.compile_and_run(script)?;
let exported: Exported = from_koto_value(koto.exports().clone())?;
println!("Exported: '{}': {:?}", exported.caption, exported.numbers);
Ok(())
}
#[derive(Deserialize, Serialize)]
struct Exported {
caption: String,
numbers: Vec<i64>,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
enum Request {
OneToFour,
FiveToEight,
}
The runtime's prelude is a KMap, which is Koto's standard hashmap type.
Values can be added to the prelude via KMap::insert, taking any Rust value
that implements Into<KValue>. Basic types like strings and numbers are
automatically converted to corresponding Koto types.
use koto::{Result, prelude::*};
fn main() -> Result<()> {
let script = "
print 'name: {name}'
print 'how_many: {how_many}'
print 'yes_or_no: {if yes_or_no then 'yes' else 'no'}'
";
let mut koto = Koto::with_settings(KotoSettings::default().inherit_io());
let prelude = koto.prelude();
prelude.insert("name", "Alice");
prelude.insert("how_many", 99);
prelude.insert("yes_or_no", true);
koto.compile_and_run(script)?;
Ok(())
}
Values can also be removed from the prelude, which can be useful if you want
to restrict the capabilities of a script.
use koto::prelude::*;
fn main() {
let mut koto = Koto::with_settings(KotoSettings::default().inherit_io());
let prelude = koto.prelude();
prelude.remove("io");
prelude.remove_path("os.command");
assert!(koto.compile_and_run("io.create('temp.txt')").is_err());
assert!(koto.compile_and_run("os.command('ls')").is_err());
assert!(koto.compile_and_run("print os.name()").is_ok());
}
The arguments that are accessible in a script from os.args can be set via
KotoSettings::with_args.
use koto::{Result, prelude::*};
fn main() -> Result<()> {
let script = "
from os import args
if (size args) > 1
for i, arg in args.enumerate()
print '{i + 1}: {arg}'
else
print 'No arguments'
";
let mut koto = Koto::with_settings(KotoSettings::default().inherit_args().inherit_io());
koto.compile_and_run(script)?;
Ok(())
}
Any Rust function that implements KotoFunction can be made available to the
Koto runtime.
use koto::{Result, prelude::*};
fn main() -> Result<()> {
let mut koto = Koto::with_settings(KotoSettings::default().inherit_io());
let prelude = koto.prelude();
prelude.insert("say_hello", say_hello);
prelude.insert("plus", plus);
let script = "
say_hello()
say_hello 'Alice'
print plus 10, 20
";
koto.compile_and_run(script)?;
Ok(())
}
koto_fn! {
fn say_hello() {
println!("Hello?");
}
fn say_hello(name: &str) {
println!("Hello, {name}");
}
fn plus(a: i64, b: i64) -> i64 {
a + b
}
}
Koto::call_function can be used to call Koto functions, or any other callable
Koto values.
use koto::{Result, prelude::*};
fn main() -> Result<()> {
let script = "
export my_fn = |a, b| '{a} + {b} is {a + b}'
";
let mut koto = Koto::default();
koto.compile_and_run(script)?;
let result = koto.call_exported_function("my_fn", &[1.into(), 2.into()])?;
println!("Result: {}", koto.value_to_string(result)?);
Ok(())
}
A module in Koto is simply a KMap, conventionally with a defined
@type.
use koto::prelude::*;
fn main() {
let script = "
from my_module import echo, square
print echo 'Hello'
print square 9
";
let mut koto = Koto::with_settings(KotoSettings::default().inherit_io());
koto.prelude().insert("my_module", make_module());
koto.compile_and_run(script).unwrap();
}
fn make_module() -> KMap {
let module = KMap::with_type("my_module");
module.add_fn("echo", |ctx| match ctx.args() {
[KValue::Str(s)] => Ok(format!("{s}!").into()),
unexpected => unexpected_args("|String|", unexpected),
});
module.add_fn("square", |ctx| match ctx.args() {
[KValue::Number(n)] => Ok((n * n).into()),
unexpected => unexpected_args("|Number|", unexpected),
});
module
}
Any Rust type that implements KotoObject can be used in the Koto runtime.
KotoObject requires KotoType, KotoCopy, and KotoAccess to be
implemented.
use koto::{Result, derive::*, prelude::*, runtime};
fn main() -> Result<()> {
let script = "
my_type = make_my_type 41
print my_type.get()
print my_type.set 99
";
let mut koto = Koto::with_settings(KotoSettings::default().inherit_io());
koto.prelude()
.add_fn("make_my_type", |ctx| match ctx.args() {
[KValue::Number(n)] => Ok(MyType::make_koto_object(*n).into()),
unexpected => unexpected_args("|Number|", unexpected),
});
koto.compile_and_run(script)?;
Ok(())
}
#[derive(Clone, Copy, KotoCopy, KotoType)]
struct MyType(i64);
#[koto_impl]
impl MyType {
fn make_koto_object(n: KNumber) -> KObject {
let my_type = Self(n.into());
KObject::from(my_type)
}
#[koto_method]
fn get(&self) -> i64 {
self.0
}
#[koto_method]
fn set(&mut self, n: i64) -> &mut Self {
self.0 = n;
self
}
}
impl KotoObject for MyType {
fn display(&self, ctx: &mut DisplayContext) -> runtime::Result<()> {
ctx.append(format!("MyType({})", self.0));
Ok(())
}
}
Runtime type checks are enabled by default, the compiler can be prevented from
emitting type check instructions by disabling the enable_type_checks flag.
use koto::{Result, prelude::*};
fn main() -> Result<()> {
let script = "
let x: String = 123
";
let mut koto = Koto::default();
let result = koto.compile_and_run(script);
assert!(result.is_err());
let result = koto.compile_and_run(CompileArgs::new(script).enable_type_checks(false));
assert!(result.is_ok());
Ok(())
}
By default, Koto's runtime is single-threaded, and many of its core types (e.g. KValue) don't
implement Send or Sync.
For applications that need to support multi-threaded scripting, the arc feature switches from an
Rc<RefCell<T>>-based memory strategy to one using Arc<RwLock<T>>.
Only one memory strategy can be enabled at a time, so default features need to be disabled.
[dependencies.koto]
version = "0.15"
default-features = false
features = ["arc"]
Sometimes it can be useful to have more that one Koto instance, while avoiding the setup cost of
initializing the core library, prelude, module cache, and other resources that could be shared
between instances.
Koto::spawn_shared provides this feature, spawning a runtime with shared settings and resources.
An exception to resource sharing is the spawned runtime's exports map, which is newly initialized
with the assumption that the spawned instance will be used to initialize new modules.
use anyhow::{Result, bail};
use koto::prelude::*;
fn main() -> Result<()> {
let script = "
export x = a + b
";
let mut koto = Koto::with_settings(KotoSettings::default().inherit_io());
koto.prelude().insert("a", 3);
koto.prelude().insert("b", 4);
koto.compile_and_run(script).unwrap();
let Some(KValue::Number(x1)) = koto.exports().get("x") else {
bail!("Expected 7");
};
let mut koto2 = koto.spawn_shared();
koto2.prelude().insert("a", 10);
assert!(koto2.exports().is_empty());
koto2.compile_and_run(script).unwrap();
let Some(KValue::Number(x2)) = koto2.exports().get("x") else {
bail!("Expected 14");
};
println!("x1: {x1}");
println!("x2: {x2}");
Ok(())
}
Some applications (like REPLs) require assigned variables to persist between each script evaluation.
This can be achieved by enabling the export_top_level_ids flag,
which will result in all top-level assignments being exported.
use koto::{Result, prelude::*};
fn main() -> Result<()> {
let mut koto = Koto::default();
koto.compile_and_run(CompileArgs::new("x = 1").export_top_level_ids(true))?;
assert!(koto.exports().get("x").is_some());
match koto.compile_and_run(CompileArgs::new("x + x").export_top_level_ids(true))? {
KValue::Number(result) => assert_eq!(result, KNumber::from(2)),
unexpected => unexpected_type("Number", &unexpected)?,
}
Ok(())
}