Language Reference
Ion is an embeddable scripting language for Rust applications. It has Rust-flavored syntax with pattern matching, Result/Option types, immutable-by-default variables, and first-class JSON/dict support.
| Type | Literal | Example |
|---|---|---|
| Int | 42, -1, 0 | let x = 42; |
| Float | 3.14, -0.5 | let pi = 3.14; |
| Bool | true, false | let ok = true; |
| String | "hello" | let s = "hello"; |
| Unit | () | let u = (); |
| List | [a, b, c] | let xs = [1, 2, 3]; |
| Tuple | (a, b, c) | let t = (1, "hi", true); |
| Dict | #{key: val} | let d = #{name: "ion"}; |
| Option | Some(x), None | let opt = Some(42); |
| Result | Ok(x), Err(e) | let res = Ok(1); |
| Bytes | b"hello", b"\x00\xff" | let data = b"\x48\x49"; |
| Function | fn, |x| x | let f = |x| x * 2; |
All values support .to_string() for conversion to string. Use type_of(value) to inspect types at runtime:
type_of(42) // "int"type_of("hello") // "string"type_of(true) // "bool"type_of([]) // "list"type_of(#{}) // "dict"type_of(()) // "unit"type_of(3.14) // "float"Variables
Section titled “Variables”Variables are immutable by default. Use mut for mutable bindings.
let x = 10; // immutablelet mut y = 0; // mutabley = 5; // OKy += 1; // compound assignment// x = 1; // ERROR: x is not mutableCompound assignment operators
Section titled “Compound assignment operators”+=, -=, *=, /=
Destructuring
Section titled “Destructuring”let (a, b) = (1, 2);let [x, y, z] = [10, 20, 30];Operators
Section titled “Operators”Arithmetic
Section titled “Arithmetic”+, -, *, /, % (modulo)
Comparison
Section titled “Comparison”==, !=, <, >, <=, >=
Logical
Section titled “Logical”&& (and), || (or), ! (not)
Bitwise
Section titled “Bitwise”& (and), | (or), ^ (xor), << (left shift), >> (right shift)
- (negate), ! (not)
5 |> double // equivalent to double(5)Try (?)
Section titled “Try (?)”Unwraps Ok/Some, propagates Err/None (see Error Handling).
Control Flow
Section titled “Control Flow”If / else
Section titled “If / else”if/else is an expression — it returns a value.
let result = if x > 0 { "positive" } else { "non-positive" };Chained:
if x < 0 { -1} else if x == 0 { 0} else { 1}If-let
Section titled “If-let”Destructure and branch on Option/Result:
let opt = Some(10);if let Some(v) = opt { v + 1} else { 0}let mut x = 0;while x < 10 { x += 1;}While-let
Section titled “While-let”let mut items = Some(1);while let Some(v) = items { // process v items = None;}Iterates over lists, tuples, dicts, strings, bytes, and ranges.
for x in [1, 2, 3] { io::println(x);}
for (key, val) in #{a: 1, b: 2} { io::println(key, val);}
for ch in "hello" { io::println(ch);}Infinite loop. Use break to exit, optionally with a value.
let result = loop { if done { break 42; }};Break / Continue
Section titled “Break / Continue”for x in [1, 2, 3, 4, 5] { if x == 3 { continue; } if x == 5 { break; } io::println(x);}Return
Section titled “Return”fn find_first(items) { for item in items { if item > 10 { return item; } } return None;}Functions
Section titled “Functions”Declaration
Section titled “Declaration”fn add(a, b) { a + b}The last expression is the return value (no semicolon needed).
Default parameters
Section titled “Default parameters”fn greet(name = "world") { f"hello {name}"}greet() // "hello world"greet("ion") // "hello ion"Named arguments
Section titled “Named arguments”Call functions with arguments specified by name:
fn connect(host, port, timeout = 30) { f"{host}:{port} (timeout={timeout})"}connect(port: 443, host: "example.com")connect("localhost", timeout: 5, port: 8080)Positional and named arguments can be mixed. Named arguments are resolved to parameter positions at call time.
Variadic and shaped parameters
Section titled “Variadic and shaped parameters”Use *name to collect extra positional arguments into a list, and **name to collect extra keyword arguments into a dict:
fn summarize(first, *rest, **opts) { #{first: first, rest: rest, opts: opts}}
summarize(1, 2, 3, mode: "fast")// #{first: 1, rest: [2, 3], opts: #{mode: "fast"}}Call sites can spread a list into positional arguments with *expr and a dict into keyword arguments with **expr:
fn score(a, b, c) { a * 100 + b * 10 + c }score(*[1, 2], c: 3) // 123Method calls accept positional arguments and *expr list spreads. Keyword arguments are only for callable values, not methods.
Use / to mark earlier parameters positional-only, and a bare * to make later parameters keyword-only:
fn shaped(a, /, b, *, c) { a + b + c}
shaped(1, b: 2, c: 3)Lambdas (closures)
Section titled “Lambdas (closures)”let double = |x| x * 2;let add = |a, b| a + b;Lambdas capture variables from their enclosing scope:
let x = 10;let add_x = |y| x + y;add_x(5) // 15Higher-order functions
Section titled “Higher-order functions”fn make_adder(x) { |y| x + y}let add5 = make_adder(5);add5(10) // 15Recursion
Section titled “Recursion”fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) }}fib(10) // 55Tail calls are optimized (TCO) on the bytecode VM path.
Pattern Matching
Section titled “Pattern Matching”Match expression
Section titled “Match expression”match value { pattern => result, pattern => result,}Pattern types
Section titled “Pattern types”// Literal patternsmatch x { 0 => "zero", 1 => "one", _ => "other", // wildcard}
// Variable bindingmatch x { n => n + 1, // binds n to x}
// Option patternsmatch opt { Some(v) => v * 2, None => 0,}
// Result patternsmatch res { Ok(v) => v, Err(e) => e,}
// Tuple patternsmatch (1, 2) { (a, b) => a + b,}
// List patternsmatch items { [] => "empty", [a] => f"one: {a}", [a, b, ...rest] => f"first: {a}", // rest pattern}Guards
Section titled “Guards”match x { n if n > 0 => "positive", n if n < 0 => "negative", _ => "zero",}Collections
Section titled “Collections”let xs = [1, 2, 3];xs[0] // 1 (0-indexed)xs[-1] // 3 (negative indexing)xs[1..3] // [2, 3] (slicing)xs[..2] // [1, 2]xs[1..] // [2, 3]See List Methods for the full API.
Tuples
Section titled “Tuples”let t = (1, "hello", true);t.0 // 1 (field access by index)t.1 // "hello"let d = #{name: "ion", version: 1};d.name // "ion" (field access)d["name"] // "ion" (bracket access)d.missing // None (missing key returns None)String keys and shorthand identifier keys are equivalent:
#{name: 1} // same as #{"name": 1}Computed keys use expressions:
let key = "x";#{(key): 42} // #{"x": 42}Spread operator:
let base = #{a: 1, b: 2};let extended = #{...base, c: 3}; // #{a: 1, b: 2, c: 3}See Dict Methods for the full API.
Comprehensions
Section titled “Comprehensions”// List comprehension[x * 2 for x in [1, 2, 3]] // [2, 4, 6]
// Filtered comprehension[x for x in [1, 2, 3, 4, 5] if x > 3] // [4, 5]
// Dict comprehension#{k: v * 10 for (k, v) in #{a: 1, b: 2}}Strings
Section titled “Strings”Literals
Section titled “Literals”"hello world""line one\nline two" // escape sequences: \n, \t, \r, \\, \""\u{1F600}" // Unicode escape: 😀Triple-quoted strings
Section titled “Triple-quoted strings”Multi-line strings using triple quotes:
let text = """This is a multi-linestring literal.""";A leading newline immediately after """ is stripped. Escape sequences work inside triple-quoted strings.
Triple-quoted f-strings: f"""value: {x}"""
F-strings (interpolation)
Section titled “F-strings (interpolation)”let name = "ion";f"hello {name}" // "hello ion"f"2 + 2 = {2 + 2}" // "2 + 2 = 4"f"result: {foo("bar")}" // nested quotes in interpolationIndexing
Section titled “Indexing”"hello"[0] // "h""hello"[4] // "o""hello"[-1] // "o" (negative indexing)Concatenation and repetition
Section titled “Concatenation and repetition”"hello" + " " + "world" // "hello world""ha" * 3 // "hahaha"3 * "ab" // "ababab"See String Methods for the full API.
Error Handling
Section titled “Error Handling”Option
Section titled “Option”Represents an optional value: Some(value) or None.
let opt = Some(42);let empty = None;Result
Section titled “Result”Represents success or failure: Ok(value) or Err(error).
let ok = Ok(42);let err = Err("something failed");The ? operator
Section titled “The ? operator”? unwraps Ok/Some or early-returns Err/None:
fn parse_and_double(s) { let n = s.to_int()?; // propagates Err if parse fails Ok(n * 2)}parse_and_double("5") // Ok(10)parse_and_double("abc") // Err("invalid digit found in string")fn first_item(items) { let v = items.first()?; // propagates None if empty Some(v + 1)}At the top level (outside a function), ? on Err/None returns the error/none as a value rather than crashing.
Try / Catch
Section titled “Try / Catch”Catch runtime errors with try/catch blocks:
let result = try { let x = 10 / 0; x} catch e { f"caught: {e}"};// result is the error message stringtry/catch is an expression — the last value of whichever branch executes is returned. Control flow signals (return, break, continue) pass through and are not caught.
// Nested try/catchtry { try { error_fn() } catch inner { f"inner: {inner}" }} catch outer { f"outer: {outer}"}Method chains
Section titled “Method chains”Both Option and Result support functional chaining:
Some(5).unwrap() // 5Some(5).map(|x| x * 2) // Some(10)None.unwrap_or(0) // 0Ok(5).unwrap() // 5Ok(5).map(|x| x + 1) // Ok(6)Err("fail").unwrap_or(0) // 0See Option Methods and Result Methods.
Builtin Functions
Section titled “Builtin Functions”Type conversion
Section titled “Type conversion”| Function | Description |
|---|---|
str(x) | Convert to string |
int(x) | Convert to int (from float, string, bool) |
float(x) | Convert to float (from int, string) |
type_of(x) | Returns type name as string |
Collections
Section titled “Collections”| Function | Description |
|---|---|
len(x) | Length of list, string, dict, or bytes |
range(n) | [0, 1, ..., n-1] |
range(start, end) | [start, start+1, ..., end-1] |
enumerate(val) | [(0, a), (1, b), ...] (list, string, or dict) |
set(list) | Create a set (deduplicates) |
cell(value) | Mutable reference cell for shared closure state |
Assertions
Section titled “Assertions”| Function | Description |
|---|---|
assert(cond) | Error if cond is false |
assert(cond, msg) | Error with custom message if cond is false |
assert_eq(a, b) | Error if a != b |
assert_eq(a, b, msg) | Error with custom message if a != b |
| Function | Description |
|---|---|
bytes() | Empty bytes |
bytes(list) | Bytes from list of ints (0-255) |
bytes(string) | Bytes from UTF-8 string |
bytes(n) | Zero-filled bytes of length n |
bytes_from_hex(string) | Bytes from hex string |
Bytes Module
Section titled “Bytes Module”| Function | Description |
|---|---|
bytes::new() | Empty bytes |
bytes::zeroed(n) | Zero-filled bytes of length n |
bytes::repeat(byte, n) | Bytes filled with a repeated byte |
bytes::from_list(list) | Bytes from list of ints (0-255) |
bytes::from_str(string) | Bytes from UTF-8 string |
bytes::from_hex(string) | Result containing bytes decoded from hex |
bytes::from_base64(string) | Result containing bytes decoded from Base64 |
bytes::concat(list) | Concatenate a list of bytes |
bytes::join(list, sep?) | Join a list of bytes with an optional bytes separator |
bytes::u16_le(n) / bytes::u16_be(n) | Pack unsigned 16-bit integer |
bytes::u32_le(n) / bytes::u32_be(n) | Pack unsigned 32-bit integer |
bytes::u64_le(n) / bytes::u64_be(n) | Pack unsigned 64-bit integer that fits in int |
bytes::i16_le(n) / bytes::i16_be(n) | Pack signed 16-bit integer |
bytes::i32_le(n) / bytes::i32_be(n) | Pack signed 32-bit integer |
bytes::i64_le(n) / bytes::i64_be(n) | Pack signed 64-bit integer |
Methods
Section titled “Methods”All runtime values support .to_string(). The tables below list
type-specific methods and repeat .to_string() where the stdlib manifest lists
it explicitly.
String Methods
Section titled “String Methods”| Method | Returns | Description |
|---|---|---|
.len() | Int | Byte length |
.char_len() | Int | Character count (for Unicode) |
.is_empty() | Bool | True if empty |
.contains(sub) | Bool | Contains substring (string or char code int) |
.starts_with(pre) | Bool | Starts with prefix |
.ends_with(suf) | Bool | Ends with suffix |
.find(sub) | Option(Int) | Char index of first occurrence |
.index(sub) | Option(Int) | Alias for .find(sub) |
.count(sub) | Int | Count non-overlapping occurrences |
.trim() | String | Strip leading/trailing whitespace |
.trim_start() | String | Strip leading whitespace |
.trim_end() | String | Strip trailing whitespace |
.to_upper() | String | Uppercase |
.to_lower() | String | Lowercase |
.split(delim) | List | Split by delimiter |
.replace(from, to) | String | Replace all occurrences |
.chars() | List | List of single-char strings |
.pad_start(n, ch?) | String | Left-pad to width n (default space) |
.pad_end(n, ch?) | String | Right-pad to width n (default space) |
.strip_prefix(s) | String | Remove prefix if present |
.strip_suffix(s) | String | Remove suffix if present |
.reverse() | String | Reversed string |
.repeat(n) | String | Repeat n times |
.slice(start, end) | String | Substring by char index |
.bytes() | List | List of byte values (ints) |
.to_int() | Result | Parse as integer |
.to_float() | Result | Parse as float |
.to_string() | String | Convert to string |
List Methods
Section titled “List Methods”| Method | Returns | Description |
|---|---|---|
.len() | Int | Number of elements |
.is_empty() | Bool | True if empty |
.first() | Option | First element |
.last() | Option | Last element |
.contains(val) | Bool | Contains value |
.push(val) | List | New list with value appended |
.pop() | (List, Option) | New list and removed last element |
.sort() | List | Sorted copy |
.reverse() | List | Reversed copy |
.flatten() | List | Flatten one level of nesting |
.join(sep) | String | Join elements with separator |
.enumerate() | List | [(0, a), (1, b), ...] |
.zip(other) | List | [(a1, b1), (a2, b2), ...] |
.map(fn) | List | Apply function to each element |
.filter(fn) | List | Keep elements where fn returns true |
.fold(init, fn) | Value | Reduce with accumulator |
.flat_map(fn) | List | Map then flatten results |
.any(fn) | Bool | True if fn returns true for any element |
.all(fn) | Bool | True if fn returns true for all elements |
.index(val) | Option(Int) | Index of first occurrence |
.count(val) | Int | Number of occurrences |
.slice(start, end?) | List | Sublist by index |
.dedup() | List | Remove consecutive duplicates |
.unique() | List | Remove all duplicates (preserves order) |
.min() | Option | Minimum element |
.max() | Option | Maximum element |
.sum() | Int/Float | Sum of numeric elements |
.window(n) | List | Sliding windows of size n |
.chunk(n) | List | Split into chunks of size n |
.reduce(fn) | Value | Reduce without an initial value |
.sort_by(fn) | List | Sort with custom comparator (fn returns int: neg/0/pos) |
.to_string() | String | Convert to string |
Tuple Methods
Section titled “Tuple Methods”| Method | Returns | Description |
|---|---|---|
.len() | Int | Number of elements |
.contains(val) | Bool | Contains value |
.to_list() | List | Convert to list |
.to_string() | String | Convert to string |
Set Methods
Section titled “Set Methods”| Method | Returns | Description |
|---|---|---|
.len() | Int | Number of elements |
.is_empty() | Bool | True if empty |
.contains(val) | Bool | Contains value |
.add(val) | Set | New set with value added |
.remove(val) | Set | New set with value removed |
.union(other) | Set | Union of two sets |
.intersection(other) | Set | Intersection of two sets |
.difference(other) | Set | Difference of two sets |
.to_list() | List | Convert to list |
.to_string() | String | Convert to string |
Range Methods
Section titled “Range Methods”| Method | Returns | Description |
|---|---|---|
.len() | Int | Number of values in the range |
.contains(n) | Bool | True if the integer is in range |
.to_list() | List | Materialize the range as a list |
.to_string() | String | Convert to string |
Dict Methods
Section titled “Dict Methods”| Method | Returns | Description |
|---|---|---|
.len() | Int | Number of entries |
.is_empty() | Bool | True if empty |
.keys() | List | List of keys |
.values() | List | List of values |
.entries() | List | List of (key, value) tuples |
.contains_key(key) | Bool | Key exists |
.get(key) | Option | Get value by key |
.update(other) | Dict | Merge other dict (overwrites existing keys) |
.keys_of(val) | List | Keys with the given value |
.insert(key, val) | Dict | New dict with entry added |
.remove(key) | Dict | New dict with entry removed |
.merge(other) | Dict | New dict merging two dicts |
.map(fn) | Dict | Apply fn(key, value) to each entry, keep keys |
.filter(fn) | Dict | Keep entries where fn(key, value) is truthy |
.zip(other) | Dict | Merge matching keys into tuples |
.to_string() | String | Convert to string |
Option Methods
Section titled “Option Methods”| Method | Returns | Description |
|---|---|---|
.is_some() | Bool | True if Some |
.is_none() | Bool | True if None |
.unwrap() | Value | Extract value, error if None |
.unwrap_or(default) | Value | Unwrap or return default |
.expect(msg) | Value | Unwrap or error with message |
.map(fn) | Option | Apply fn to inner value |
.and_then(fn) | Value | Flat-map (fn should return Option) |
.or_else(fn) | Value | Call fn if None |
.unwrap_or_else(fn) | Value | Unwrap or call fn |
.to_string() | String | Convert to string |
Result Methods
Section titled “Result Methods”| Method | Returns | Description |
|---|---|---|
.is_ok() | Bool | True if Ok |
.is_err() | Bool | True if Err |
.unwrap() | Value | Extract value, error if Err |
.unwrap_or(default) | Value | Unwrap or return default |
.expect(msg) | Value | Unwrap or error with message |
.map(fn) | Result | Apply fn to Ok value |
.map_err(fn) | Result | Apply fn to Err value |
.and_then(fn) | Value | Flat-map on Ok |
.or_else(fn) | Value | Flat-map on Err |
.unwrap_or_else(fn) | Value | Unwrap or call fn with error |
.to_string() | String | Convert to string |
Bytes Methods
Section titled “Bytes Methods”| Method | Returns | Description |
|---|---|---|
.len() | Int | Number of bytes |
.is_empty() | Bool | True if empty |
.bytes() | Bytes | Identity byte representation |
.contains(byte_or_bytes) | Bool | Contains byte value or bytes sequence |
.find(byte_or_bytes) | Option(Int) | Index of first occurrence |
.count(byte_or_bytes) | Int | Number of non-overlapping occurrences |
.starts_with(prefix) | Bool | Starts with byte value or bytes prefix |
.ends_with(suffix) | Bool | Ends with byte value or bytes suffix |
.slice(start, end) | Bytes | Sub-slice |
.split(sep) | List | Split by byte value or bytes separator |
.replace(from, to) | Bytes | Replace byte value or bytes sequence |
.reverse() | Bytes | Reversed copy |
.repeat(n) | Bytes | Repeat n times |
.push(byte) | Bytes | New bytes with byte appended |
.extend(other) | Bytes | New bytes with other bytes appended |
.set(index, byte) | Bytes | New bytes with byte replaced at index |
.pop() | (Bytes, Option(Int)) | New bytes and removed last byte |
.to_list() | List | List of int values |
.to_str() | Result | Decode as UTF-8 |
.to_hex() | String | Hex-encoded string |
.to_base64() | String | Base64-encoded string |
.read_u16_le(offset) / .read_u16_be(offset) | Result | Read unsigned 16-bit integer |
.read_u32_le(offset) / .read_u32_be(offset) | Result | Read unsigned 32-bit integer |
.read_u64_le(offset) / .read_u64_be(offset) | Result | Read unsigned 64-bit integer if it fits in int |
.read_i16_le(offset) / .read_i16_be(offset) | Result | Read signed 16-bit integer |
.read_i32_le(offset) / .read_i32_be(offset) | Result | Read signed 32-bit integer |
.read_i64_le(offset) / .read_i64_be(offset) | Result | Read signed 64-bit integer |
.to_string() | String | Convert to string |
Cell Methods
Section titled “Cell Methods”A Cell is a shared mutable reference cell, created with the top-level builtin cell(value). It enables shared mutable state across closures.
let c = cell(0);c.get() // 0c.set(42);c.get() // 42c.update(|v| v + 1);c.get() // 43| Method | Returns | Description |
|---|---|---|
.get() | Value | Read the current value |
.set(v) | Unit | Replace the stored value with v |
.update(fn) | Unit | Apply fn to the current value and store the result |
.to_string() | String | Convert to string |
Binary data type for raw byte manipulation.
let data = b"hello"; // bytes literallet hex = b"\x48\x49"; // hex escapelet empty = bytes(); // empty byteslet from_list = bytes([72, 73]); // from intslet decoded = bytes::from_hex("deadbeef").unwrap();let word = bytes::from_base64("aGVsbG8=").unwrap();let packed = bytes::u32_be(305419896);
data[0] // 72 (int)data[0..3] // b"hel" (slicing)data.len() // 5data.to_hex() // "68656c6c6f"data.to_str() // Ok("hello")packed.read_u16_be(0) // Ok(4660)Concurrency
Section titled “Concurrency”Native async host integration requires the
async-runtimecargo feature.
Ion provides structured concurrency with async blocks, spawn, .await,
select, timers, and channels. In the native async runtime, Ion code stays
synchronous-looking: a script calls an async host function as a normal
function, and the runtime parks the current bytecode continuation on the
underlying Tokio future.
engine.register_async_fn("http_get", |args| async move { let url = args[0].as_str().unwrap_or("").to_string(); // reqwest::get(url).await?.text().await, mapped into Value Ok(Value::Str(url))});fn load_user(id) { // No `await` at this call site. If `http_get` is a host async // function, eval_async parks and resumes this Ion function. json::decode(http_get(f"/users/{id}"))}Async blocks
Section titled “Async blocks”let result = async { let t1 = spawn compute(1); let t2 = spawn compute(2); t1.await + t2.await};All spawned tasks must complete before the async block returns.
Spawned tasks are Ion tasks managed by the runtime, not OS threads.
Spawn and await
Section titled “Spawn and await”let task = spawn expensive_work();// ... do other things ...let result = task.await;spawn is only valid inside an async {} block. Awaiting a task parks
the caller until the child completes. If a child fails before it is awaited,
the nursery cancels sibling tasks and propagates the error.
| Method | Description |
|---|---|
task.await | Wait for the task result |
task.await_timeout(ms) | Wait up to ms, returning Option |
task.is_finished() | Check whether the task has completed |
task.cancel() | Cancel the task |
task.is_cancelled() | Check whether the task was cancelled |
task.to_string() | Convert the task handle to a string |
Channels
Section titled “Channels”let (tx, rx) = channel(10); // buffered channel, capacity 10tx.send(42);let val = rx.recv(); // parks until value availabletx.close();| Method | Description |
|---|---|
tx.send(val) | Send a value, parking if the bounded channel is full |
tx.close() | Close the sender |
rx.recv() | Receive, parking until a value arrives; returns None when closed |
rx.try_recv() | Non-blocking receive, returns Option |
rx.recv_timeout(ms) | Receive with timeout, returns Option |
rx.close() | Close the receiver |
tx.to_string() / rx.to_string() | Convert the channel endpoint to a string |
Under async-runtime, channels are backed by Tokio channels and integrate
with the same parking mechanism as async host functions.
Select
Section titled “Select”Race multiple async operations:
select { val = spawn http_get("/fast") => f"fast: {val}", _ = spawn sleep(250) => "timeout",}The first completed branch wins. Losing branch tasks are cancelled and dropped.
Timers and timeout
Section titled “Timers and timeout”sleep(50); // parks on a Tokio timer under eval_async
let maybe_body = timeout(250, || http_get("/slow"));match maybe_body { Some(body) => body, None => "request timed out",}timeout(ms, fn) returns Some(value) if the callback completes before
the timer, or None if the timer wins. Callback errors still propagate.
Tokio host callbacks
Section titled “Tokio host callbacks”Host async functions can call back into Ion through EngineHandle:
let handle = engine.handle();engine.register_async_fn("wait_for_event", move |_args| { let handle = handle.clone(); async move { let event = Value::Str("ready".into()); handle.call_async("on_event", vec![event]).await }});The callback runs on the same local async runtime and may itself call async host functions.
Modules
Section titled “Modules”Modules provide namespaced access to functions and values registered by the host application.
Module path access
Section titled “Module path access”Use :: to access module members:
let result = math::add(1, 2);let pi = math::PI;Nested submodules:
let resp = net::http::get("https://example.com");Use imports
Section titled “Use imports”Import specific names into the current scope:
use math::add; // single importuse math::{add, PI}; // multiple importsuse math::*; // glob import (all names)After importing, names can be used directly:
use math::add;add(1, 2) // 3 (no need for math::add)Aliased imports
Section titled “Aliased imports”Use as to rebind an imported name to a different local name. Aliases work for
single imports and inside braced lists; glob imports cannot be aliased.
use io::println as say; // single import with aliasuse math::add as sum;use math::{add as sum, PI}; // mixed: some aliased, some notuse math::{add as sum, PI as pi}; // all aliasedAfter aliasing, only the alias is in scope — the original name is not bound:
use math::add as sum;sum(1, 2) // 3add(1, 2) // error: undefined name 'add'Submodule imports
Section titled “Submodule imports”use net::http::get; // import from nested moduleuse net::http::get as fetch; // nested import with aliasuse net::http::*; // glob import from nested moduleStandard library modules
Section titled “Standard library modules”The following modules are available by default in every Engine:
| Name | Description |
|---|---|
math::PI | Pi (3.14159…) |
math::E | Euler’s number (2.71828…) |
math::TAU | Tau (2π) |
math::INF | Positive infinity |
math::NAN | Not-a-number |
math::abs(x) | Absolute value |
math::min(a, b, ...) | Minimum |
math::max(a, b, ...) | Maximum |
math::floor(x) | Floor |
math::ceil(x) | Ceiling |
math::round(x) | Round to nearest |
math::sqrt(x) | Square root |
math::pow(base, exp) | Exponentiation |
math::clamp(val, lo, hi) | Clamp to range |
math::sin(x) | Sine |
math::cos(x) | Cosine |
math::tan(x) | Tangent |
math::atan2(y, x) | Two-argument arctangent |
math::log(x) | Natural logarithm |
math::log2(x) | Base-2 logarithm |
math::log10(x) | Base-10 logarithm |
math::is_nan(x) | Check if NaN |
math::is_inf(x) | Check if infinite |
| Name | Description |
|---|---|
json::encode(value) | Value to JSON string |
json::decode(string) | JSON string to value |
json::pretty(value) | Pretty-printed JSON string |
json::msgpack_encode(value) | Value to MessagePack bytes (requires the msgpack feature) |
json::msgpack_decode(bytes) | MessagePack bytes to value (requires the msgpack feature) |
Random values, ranges, and collection sampling. Integer ranges are half-open:
rand::int(10) returns a value in 0..10, and rand::int(5, 10) returns a
value in 5..10.
| Name | Description |
|---|---|
rand::int() | Random signed integer |
rand::int(max) | Random integer in 0..max |
rand::int(min, max) | Random integer in min..max |
rand::float() | Random float in 0.0..1.0 |
rand::float(max) | Random float in 0.0..max |
rand::float(min, max) | Random float in min..max |
rand::bool() | Random boolean |
rand::bool(probability) | True with probability 0.0..=1.0 |
rand::bytes(n) | Random bytes of length n |
| `rand::choice(list | string |
| `rand::shuffle(list | bytes)` |
| `rand::sample(list | bytes, n)` |
| Name | Description |
|---|---|
io::print(args...) | Print without newline |
io::println(args...) | Print with newline |
io::eprintln(args...) | Print to stderr with newline |
Embedding hosts must install an OutputHandler on the engine before
scripts can use io::print, io::println, or io::eprintln.
string
Section titled “string”| Name | Description |
|---|---|
string::len(s) | Alias for s.len() |
string::char_len(s) | Alias for s.char_len() |
string::is_empty(s) | Alias for s.is_empty() |
string::contains(s, sub) | Alias for s.contains(sub) |
string::starts_with(s, pre) | Alias for s.starts_with(pre) |
string::ends_with(s, suf) | Alias for s.ends_with(suf) |
string::find(s, sub) | Alias for s.find(sub) |
string::index(s, sub) | Alias for s.index(sub) |
string::count(s, sub) | Alias for s.count(sub) |
string::trim(s) | Alias for s.trim() |
string::trim_start(s) | Alias for s.trim_start() |
string::trim_end(s) | Alias for s.trim_end() |
string::to_upper(s) | Alias for s.to_upper() |
string::to_lower(s) | Alias for s.to_lower() |
string::split(s, delim) | Alias for s.split(delim) |
string::replace(s, from, to) | Alias for s.replace(from, to) |
string::chars(s) | Alias for s.chars() |
string::pad_start(s, n, ch?) | Alias for s.pad_start(n, ch?) |
string::pad_end(s, n, ch?) | Alias for s.pad_end(n, ch?) |
string::strip_prefix(s, pre) | Alias for s.strip_prefix(pre) |
string::strip_suffix(s, suf) | Alias for s.strip_suffix(suf) |
string::reverse(s) | Alias for s.reverse() |
string::repeat(s, n) | Alias for s.repeat(n) |
string::slice(s, start, end?) | Alias for s.slice(start, end?) |
string::bytes(s) | Alias for s.bytes() |
string::to_int(s) | Alias for s.to_int() |
string::to_float(s) | Alias for s.to_float() |
string::to_string(x) | Convert a value to a string |
string::join(list, sep?) | Join list elements into a string with optional separator |
semver
Section titled “semver”Semantic version parsing, comparison, and constraint matching. Versions
cross the language boundary as dicts shaped #{major, minor, patch, pre, build}
so scripts can inspect fields directly. Most functions accept either a string
or a parsed dict — parse once for hot loops.
| Name | Description |
|---|---|
semver::parse(s) | Parse a version string into #{major, minor, patch, pre, build}. Errors on invalid input. |
semver::is_valid(s) | true if the string parses as a valid semantic version. |
semver::format(v) | Render a version (string or dict) back to its canonical string form. |
semver::compare(a, b) | Three-way ordering: returns -1, 0, or 1. |
semver::eq(a, b) | true if a == b (including pre-release). |
semver::gt(a, b) / gte / lt / lte | Boolean comparators. |
semver::satisfies(v, req) | true if v matches the requirement string (e.g. ^1.0, ~1.2, >=1.0, <2.0). |
semver::bump_major(v) | Increment major; zero minor and patch; clear pre-release and build. |
semver::bump_minor(v) | Increment minor; zero patch; clear pre-release and build. |
semver::bump_patch(v) | Increment patch — or strip pre-release if present (1.2.3-alpha → 1.2.3). |
use semver::*;
let v = parse("1.2.3-alpha.1+build.42")?;v["major"] // 1satisfies("1.5.0", "^1.0") // truecompare("1.2.3", "1.2.4") // -1bump_major("1.2.3-alpha") // "2.0.0"The module is enabled by default. Embedders can opt out by depending on
ion-core with default-features = false.
OS / arch detection, environment variables, and process info. Pure-std,
no extra dependencies. Detection values are module-level constants
(read with os::name, no parens).
| Name | Description |
|---|---|
os::name | Target OS — "linux", "macos", "windows", "freebsd", … |
os::arch | Target architecture — "x86_64", "aarch64", "arm", … |
os::family | OS family — "unix" or "windows". |
os::pointer_width | Pointer width in bits (32 or 64). |
os::dll_extension | Dynamic-library extension without dot — "so", "dylib", "dll". |
os::exe_extension | Executable extension without dot — "" on Unix, "exe" on Windows. |
os::env_var(name [, default]) | Read an env var. Errors if missing; the optional 2nd arg is returned instead. |
os::has_env_var(name) | true if the named env var is set. |
os::env_vars() | Snapshot of all env vars as a dict<string, string>. |
os::cwd() | Current working directory. |
os::pid() | Current process id. |
os::args() | Script arguments (host-injected via Engine::set_args; default []). |
os::temp_dir() | Platform temporary directory. |
use os::*;
io::println(os::name, os::arch, os::family);
let home = env_var("HOME", "/");let port_str = env_var("PORT", "8080");
if os::family == "unix" { io::println("running on unix");}
io::println("script args:", args());os::args() reflects whatever the host passed to Engine::set_args. The
ion CLI populates it with whatever follows the script path —
ion script.ion alpha beta makes os::args() return ["alpha", "beta"].
The module is enabled by default. Embedders can opt out by depending on
ion-core with default-features = false (e.g. to forbid scripts from
reading environment variables).
Pure-string path manipulation. No I/O, no external dependencies, always
available — these are composition primitives for working with paths
before (or after) handing them to fs:: or the host. All operations work
on strings and return strings; the platform separator is exposed as
path::sep so scripts can stay portable.
| Name | Description |
|---|---|
path::sep | Platform path separator — / on Unix, \ on Windows. |
path::join(a, b, ...) | Variadic join using the platform separator. |
path::parent(p) | Directory containing p. Empty string if p has no parent. |
path::basename(p) | Final component of p. |
path::stem(p) | Basename of p with the extension stripped. |
path::extension(p) | Extension of p without the leading dot. Empty string if none. |
path::with_extension(p, ext) | Replace (or add) the extension on p. |
path::is_absolute(p) | true if p is absolute on the current platform. |
path::is_relative(p) | true if p is relative on the current platform. |
path::components(p) | Split p into its components. |
path::normalize(p) | Lexically normalise — collapse . and .. without touching the filesystem. |
use path::*;
io::println(join("src", "lib", "main.ion")); // src/lib/main.ion (Unix)io::println(extension("config.toml")); // tomlio::println(stem("config.toml")); // configio::println(with_extension("notes.md", "txt"));// notes.txtio::println(normalize("a/./b/../c")); // a/cFilesystem I/O. The script-level surface (fs::read, fs::write, …) is
identical regardless of build mode; only the underlying implementation
differs. Ion is non-coloured: scripts call these like any other function.
- In a sync build (default), each
fs::*call goes throughstd::fson the calling thread. - In an async build (
async-runtimefeature), eachfs::*call goes throughtokio::fsand cooperates with the executor instead of blocking.
The two modes are mutually exclusive at the cargo-feature level — pick
one when you build ion-core. The same script runs unchanged in either.
| Name | Description |
|---|---|
fs::read(path) | Read the file as UTF-8 text. |
fs::read_bytes(path) | Read the file as raw bytes. |
fs::write(path, contents) | Write contents (string or bytes) to path, replacing the existing file. |
fs::append(path, contents) | Append to (or create) path. |
fs::append_random(path, count) | Append count random bytes; returns bytes appended. |
fs::pad_random(path, target_size) | Grow a file to target_size with random bytes; returns bytes appended. |
fs::exists(path) | true if path exists. |
fs::is_file(path) | true if path is an existing regular file. |
fs::is_dir(path) | true if path is an existing directory. |
fs::list_dir(path) | List the entry names directly under path (non-recursive). |
fs::create_dir(path) | Create one directory; errors if a parent is missing. |
fs::create_dir_all(path) | Create the directory and any missing parents. |
fs::remove_file(path) | Delete the file at path. |
fs::remove_dir(path) | Delete the directory at path; errors if not empty. |
fs::remove_dir_all(path) | Recursively delete path and its contents. |
fs::rename(from, to) | Rename / move from to to. |
fs::copy(from, to) | Copy from to to; returns bytes copied. |
fs::metadata(path) | Returns #{size, is_file, is_dir, readonly, modified}. |
fs::canonicalize(path) | Resolve symlinks against the filesystem and normalise. |
use fs::*;use path::*;
let cfg_path = join(os::cwd(), "config.toml");if exists(cfg_path) { let raw = read(cfg_path); io::println("loaded:", raw);} else { write(cfg_path, "key = \"value\"");}
for name in list_dir(os::cwd()) { if extension(name) == "ion" { io::println(name); }}The fs feature is enabled by default. Disable it with
default-features = false for sandboxed embedders. Note that
Engine::eval is removed under async-runtime — use Engine::eval_async
in async builds. The two are deliberately mutually exclusive so non-coloured
calls like fs::read resolve to one implementation per build.
Registering modules (Rust side)
Section titled “Registering modules (Rust side)”use ion_core::module::Module;
let mut math = Module::new("math");math.register_fn("add", |args| { /* ... */ });math.set("PI", Value::Float(std::f64::consts::PI));
let mut engine = Engine::new();engine.register_module(math);With the async-runtime feature, modules can expose native async host
functions. Ion code calls them normally, but the host must use eval_async:
let mut sensor = Module::new("sensor");sensor.register_async_fn("call", |args| async move { Ok(Value::Int(args.len() as i64))});
let mut engine = Engine::new();engine.register_module(sensor);
let value = engine.eval_async(r#"sensor::call("jobs.claim", #{})"#).await?;Submodules:
let mut net = Module::new("net");let mut http = Module::new("http");http.register_fn("get", |args| { /* ... */ });net.register_submodule(http);engine.register_module(net);Embedding API
Section titled “Embedding API”Ion is designed to be embedded in Rust applications.
Basic usage
Section titled “Basic usage”use ion_core::engine::Engine;use ion_core::value::Value;
let mut engine = Engine::new();
// Evaluate a scriptlet result = engine.eval("2 + 2").unwrap();assert_eq!(result, Value::Int(4));
// Inject valuesengine.set("player_hp", Value::Int(100));engine.eval("player_hp > 50").unwrap(); // Value::Bool(true)
// Read values backlet hp = engine.get("player_hp");Register modules
Section titled “Register modules”use ion_core::module::Module;
let mut math = Module::new("math");math.register_fn("add", |args| { let (a, b) = (args[0].as_int().unwrap(), args[1].as_int().unwrap()); Ok(Value::Int(a + b))});math.set("PI", Value::Float(std::f64::consts::PI));engine.register_module(math);Scripts can then use math::add(1, 2) or use math::*;.
Register custom functions
Section titled “Register custom functions”engine.register_fn("roll_dice", |args| { let sides = args[0].as_int().ok_or("expected int")?; Ok(Value::Int(rand::random::<i64>() % sides + 1))});Host types with #[derive(IonType)]
Section titled “Host types with #[derive(IonType)]”use ion_derive::IonType;
#[derive(IonType)]struct Player { name: String, hp: i64, alive: bool,}
let mut engine = Engine::new();engine.register_type::<Player>();
// Scripts can now construct and match on Playerengine.eval(r#" let p = Player { name: "Alice", hp: 100, alive: true }; p.name"#);Host enums
Section titled “Host enums”#[derive(IonType)]enum Status { Active, Dead, Poisoned(i64),}
engine.register_type::<Status>();engine.eval(r#" match status { Status::Active => "fine", Status::Poisoned(dmg) => f"poisoned for {dmg}", Status::Dead => "dead", }"#);Typed data exchange
Section titled “Typed data exchange”let player = Player { name: "Bob".into(), hp: 50, alive: true };engine.set_typed("player", &player);
engine.eval("player.hp += 10;");
let updated: Player = engine.get_typed("player").unwrap();Execution limits
Section titled “Execution limits”use ion_core::interpreter::Limits;
engine.set_limits(Limits { max_call_depth: 256, max_loop_iters: 100_000,});Bytecode VM
Section titled “Bytecode VM”// Use the bytecode VM for better performance (requires "vm" feature)let result = engine.vm_eval("fib(30)").unwrap();The VM automatically falls back to tree-walk interpretation for unsupported features.
Cargo features
Section titled “Cargo features”| Feature | Default | Description |
|---|---|---|
vm | Yes | Bytecode VM path (vm_eval) with peephole optimization, constant folding, DCE, and TCO |
derive | Yes | #[derive(IonType)] proc macro |
async-runtime | No | Native Tokio async evaluation (eval_async, async host functions, timers, channels) |
legacy-threaded-concurrency | No | Legacy sync-eval backend using OS threads and crossbeam channels |