Getting Started with Keleusma 0.1.1
Keleusma is a Total Functional Stream Processor
that compiles to bytecode and runs on a stack-based virtual machine.
It targets no_std + alloc embedded environments
and provides definitive Worst-Case Execution Time
and Worst-Case Memory Usage guarantees through static verification.
The language enforces five guarantees at compile time.
Every function terminates.
Every stream produces output on every tick.
Every execution path has a bounded step count.
Every execution path has a bounded memory footprint.
Running code can be replaced at designated boundaries
without stopping the virtual machine.
The name derives from the Greek word keleusma, meaning a command or rhythmic signal. Ancient rowing masters used the keleusma to synchronize oarsmen on warships. The language applies the same principle to computation. Scripts execute in bounded, predictable cycles synchronized to a host-driven clock.
Keleusma targets applications where deterministic execution is a hard requirement. Audio engines must produce samples every tick without buffer underruns. Game scripting systems must complete logic updates within a fixed frame budget. Safety-critical embedded systems must guarantee worst-case timing for certification. The language addresses these domains by making unbounded computation inexpressible rather than merely discouraged.
This article provides a practical introduction to the Keleusma language and toolchain. It covers installation, the type system, the three function kinds that encode the guarantee taxonomy, pattern matching, the pipeline operator, built-in functions, the interactive read-evaluate-print loop, embedding the virtual machine in a Rust host application, hot code swapping, worst-case analysis, and bytecode compilation. Readers familiar with no_std Rust programming will find the embedding interface straightforward.
Software Versions
# Date (UTC)
$ date -u "+%Y-%m-%d %H:%M:%S +0000"
2026-03-14 10:31:00 +0000
# Keleusma Version
$ keleusma --version
keleusma 0.1.1
# Rust Version
$ cargo --version
cargo 1.88.0 (2025-06-25)
$ rustc --version
rustc 1.88.0 (2025-06-25)
Installation
Keleusma is available on crates.io
and as source on GitHub.
The command-line interface is distributed
as a separate workspace crate called keleusma-cli.
Install from Source
git clone https://github.com/sgeos/keleusma
cd keleusma
cargo install --path keleusma-cli --bin keleusma
Verify Installation
$ keleusma --version
keleusma 0.1.1
$ keleusma --help
keleusma: command-line frontend for the Keleusma scripting language
Usage:
keleusma <subcommand> [options]
keleusma <file>.kel (shorthand for `run`)
Subcommands:
run <file> Compile and execute a script
compile <file> [-o <output>] Compile to bytecode
repl Start interactive REPL
help, --help, -h Show this help
version, --version, -V Show version
Examples:
keleusma run hello.kel
keleusma hello.kel
keleusma compile hello.kel -o hello.kel.bin
keleusma repl
First Program
Create a file called hello.kel with the following contents.
fn square(x: i64) -> i64 { x * x }
fn main() -> i64 {
let a: i64 = 3;
let b: i64 = 4;
let sum_of_squares = square(a) + square(b);
sum_of_squares - 1
}
Run the program.
$ keleusma run hello.kel
24
The main function is the entry point.
It computes 3^2 + 4^2 - 1 = 9 + 16 - 1 = 24.
The program compiles to bytecode,
passes through the structural verifier,
and executes on the stack-based virtual machine.
The verifier confirms that every execution path terminates
and that memory usage is bounded
before the virtual machine begins execution.
Language Basics
Primitive Types
Keleusma provides five primitive types.
| Type | Description |
|---|---|
i64 |
64-bit signed integer |
f64 |
64-bit floating-point number |
bool |
Boolean value |
String |
UTF-8 string |
() |
Unit type |
Variables
All local bindings are immutable. Rebinding a name shadows the previous binding without mutating the original value.
let x: i64 = 42;
let y = 3.14;
let z = x + 1;
Type annotations are optional when the type can be inferred from context.
Operators
Keleusma supports standard arithmetic, comparison, and logical operators.
Arithmetic: +, -, *, /, %
Comparison: ==, !=, <, >, <=, >=
Logical: and, or, not
Type cast: as
Pipeline: |>
Logical operators use short-circuit evaluation.
The as operator performs explicit type conversion
between numeric types.
let n: i64 = 42;
let f: f64 = n as f64;
Conditionals
fn abs_value(x: i64) -> i64 {
if x < 0 { 0 - x }
else { x }
}
Conditionals are expressions and evaluate to a value. Both branches must produce the same type.
Bounded Iteration
The for loop iterates over ranges or arrays.
All for loops are bounded by construction
because ranges have a known size
and arrays have a known length.
fn main() -> i64 {
let xs = [10, 20, 30, 40];
for x in xs {
let _doubled = x * 2;
}
for i in 0..4 {
let _squared = i * i;
}
xs[3]
}
The range 0..4 produces values 0, 1, 2, and 3.
The upper bound is exclusive.
The break keyword exits a for loop early.
Three Function Kinds
The central design of Keleusma organizes all functions into three categories that collectively guarantee bounded execution. Each category has a dedicated keyword and a distinct set of obligations.
Atomic Total Functions
The fn keyword declares an atomic total function.
An atomic total function must terminate on every input.
It may not yield, may not recurse,
and must return a value.
fn clamp(val: i64, lo: i64, hi: i64) -> i64 {
if val < lo { lo }
else if val > hi { hi }
else { val }
}
The verifier confirms termination statically.
Unbounded recursion and infinite loops
are syntactically impossible in fn bodies.
Non-Atomic Total Functions
The yield keyword declares a non-atomic total function.
A non-atomic total function may yield control
back to the host one or more times
but must eventually return a final value.
yield prompt(question: String) -> String {
let answer = yield question;
answer
}
When the function executes yield question,
it suspends and sends question to the host.
The host provides a response,
which becomes the value of the yield expression.
The function must eventually reach a return point.
Productive Divergent Functions
The loop keyword declares a productive divergent function.
A productive divergent function never returns.
It runs indefinitely, processing a stream of inputs
and producing a stream of outputs.
It must yield on every iteration.
Only one loop function may exist per script.
loop main(input: i64) -> i64 {
let result = input * 2;
let input = yield result;
input
}
The verifier confirms that every control path
from the stream entry point to the reset boundary
contains at least one yield.
This guarantees productivity.
The host drives execution by providing inputs
and consuming outputs at each yield point.
Why Three Kinds
The three-kind taxonomy ensures that the verifier can assign a finite cost to every execution path. Atomic total functions have a fixed step count. Non-atomic total functions have a fixed step count per segment. Productive divergent functions have a fixed step count per tick. Together, these properties enable static computation of worst-case execution time for any single tick of the system.
Composite Types
Structs
struct Point { x: i64, y: i64 }
fn manhattan_norm(p: Point) -> i64 {
p.x + p.y
}
fn main() -> i64 {
let origin_offset = Point { x: 3, y: 4 };
manhattan_norm(origin_offset)
}
Structs group named fields into a single value. Field access uses dot notation.
Enums
enum Shape {
Circle(i64),
Rectangle(i64, i64),
Square(i64),
}
fn area_estimate(s: Shape) -> i64 {
match s {
Shape::Circle(r) => 3 * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Square(side) => side * side,
}
}
fn main() -> i64 {
let shape = Shape::Rectangle(20, 5);
area_estimate(shape)
}
Enums define a closed set of variants.
Each variant may carry associated data.
The match expression destructures enum values.
Match expressions must be exhaustive
or include a wildcard _ arm.
Tuples and Arrays
let pair: (i64, i64) = (1, 2);
let xs = [10, 20, 30, 40];
let repeated = [0.0; 8];
Tuples group heterogeneous values by position.
Arrays hold homogeneous values of fixed length.
The [value; count] syntax creates an array
filled with count copies of value.
Option Type
let some_value: Option<i64> = Option::Some(42);
let no_value: Option<i64> = Option::None;
The Option type represents a value that may or may not exist.
Pipeline Operator
The pipeline operator |> threads the result
of the left expression
as the first argument to the right function call.
fn double(x: i64) -> i64 { x + x }
fn add(x: i64, y: i64) -> i64 { x + y }
fn square(x: i64) -> i64 { x * x }
fn main() -> i64 {
6 |> double() |> add(1) |> square()
}
This program produces 169.
The value 6 flows through double to produce 12,
then through add(_, 1) to produce 13,
then through square to produce 169.
For functions where the piped value should not be the first argument, use the underscore placeholder.
value |> insert(collection, _)
The underscore marks where the piped value should be inserted.
Multiheaded Functions and Guards
A single function name may have multiple heads, each with its own parameter pattern. The runtime tries the heads in source order and dispatches to the first one whose pattern matches.
fn classify(0) -> i64 { 0 }
fn classify(1) -> i64 { 1 }
fn classify(n: i64) -> i64 { n + 10 }
fn main() -> i64 {
let a = classify(0);
let b = classify(1);
let c = classify(11);
let d = classify(7);
a + b + c + d - 28
}
This program produces 11. The first head matches only the literal 0. The second head matches only the literal 1. The third head matches any other integer.
Guard clauses refine pattern matching with additional conditions.
fn sign(x: i64) -> String when x > 0 { "positive" }
fn sign(x: i64) -> String when x < 0 { "negative" }
fn sign(x: i64) -> String { "zero" }
The when clause is evaluated
after the parameter pattern matches.
If the guard fails, dispatch continues
to the next head.
Generics and Traits
Keleusma supports generic type parameters and trait-based polymorphism. Generics are monomorphized at compile time.
fn id<T>(x: T) -> T { x }
Traits declare a set of function signatures that a type must implement.
trait Doubler {
fn double(x: i64) -> i64;
}
impl Doubler for i64 {
fn double(x: i64) -> i64 { x + x }
}
fn main() -> i64 {
let n: i64 = 42;
n.double()
}
This program produces 84. Method dispatch uses dot notation. The compiler resolves the concrete implementation at compile time through monomorphization.
Trait bounds constrain type parameters.
fn use_doubler<T: Doubler>(x: T) -> i64 {
x.double()
}
Strings and Interpolation
Keleusma distinguishes two string categories. Static strings are stored in the read-only data segment and can flow anywhere in the program. Dynamic strings are arena-allocated and cannot cross yield boundaries.
String literals produce static strings.
let msg: String = "hello";
F-string interpolation provides formatted string construction.
The f"..." syntax desugars at lex time
into chains of concat and to_string calls.
Scripts that use f-strings must declare
their dependency on these native functions.
use to_string
use concat
fn add(a: i64, b: i64) -> i64 {
a + b
}
fn main() -> String {
let name = "Keleusma";
let a: i64 = 7;
let b: i64 = 2;
f"hello, {name}! {a} plus {b} is {add(a, b)}"
}
$ keleusma run fstring.kel
hello, Keleusma! 7 plus 2 is 9
The use declarations inform the compiler
that these native functions are provided by the host.
Built-in Functions
The CLI runner registers two sets of native functions automatically.
Utility Functions
| Function | Signature | Description |
|---|---|---|
to_string |
(i64 or f64) -> String |
Convert number to string |
length |
(String or Array) -> i64 |
Get length |
concat |
(String, String) -> String |
Concatenate strings |
slice |
(String, i64, i64) -> String |
Extract substring |
println |
(String) -> () |
Print to standard output |
Math Functions
| Function | Signature | Description |
|---|---|---|
math::sin |
(f64) -> f64 |
Sine |
math::cos |
(f64) -> f64 |
Cosine |
math::pow |
(f64, f64) -> f64 |
Exponentiation |
math::abs |
(f64) -> f64 |
Absolute value |
math::min |
(f64, f64) -> f64 |
Minimum |
math::max |
(f64, f64) -> f64 |
Maximum |
math::clamp |
(f64, f64, f64) -> f64 |
Clamp to range |
math::lerp |
(f64, f64, f64) -> f64 |
Linear interpolation |
Audio Functions
| Function | Signature | Description |
|---|---|---|
audio::midi_to_freq |
(i64) -> f64 |
MIDI note to frequency in Hz |
audio::freq_to_midi |
(f64) -> i64 |
Frequency to nearest MIDI note |
audio::db_to_linear |
(f64) -> f64 |
Decibels to linear amplitude |
audio::linear_to_db |
(f64) -> f64 |
Linear amplitude to decibels |
All math and audio functions accept both i64 and f64 arguments.
Integer arguments are widened to floating-point automatically.
The REPL
The interactive read-evaluate-print loop allows experimentation without creating script files.
$ keleusma repl
> 1 + 2
3
> fn double(x: i64) -> i64 { x + x }
defined: double
> double(21)
42
> :help
:help Show this help
:quit Exit the REPL
:reset Clear all definitions
:show Show defined functions
> :quit
The REPL supports function definitions, expressions,
and colon-prefixed meta-commands.
Definitions persist across evaluations
until :reset is issued.
Embedding in Rust
Keleusma is designed to be embedded in Rust host applications. The library crate provides the full compilation pipeline and virtual machine as a library API.
Minimal Example
use keleusma::compiler::compile;
use keleusma::lexer::tokenize;
use keleusma::parser::parse;
use keleusma::vm::{DEFAULT_ARENA_CAPACITY, Vm, VmState};
use keleusma::{Arena, Value};
fn main() {
let source = r#"
fn double(x: i64) -> i64 { x * x }
fn main(n: i64) -> i64 { n |> double() }
"#;
let tokens = tokenize(source).expect("lex");
let program = parse(&tokens).expect("parse");
let module = compile(&program).expect("compile");
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("verify");
match vm.call(&[Value::Int(21)]).unwrap() {
VmState::Finished(Value::Int(n)) => println!("{}", n),
_ => unreachable!(),
}
}
The host owns the arena allocator. The virtual machine borrows the arena for its operand stack, call frames, and dynamic string storage. The default arena capacity is 64 kilobytes.
Registering Native Functions
Native functions allow scripts to call into host code. The ergonomic typed interface is the recommended approach.
vm.register_fn("add", |a: i64, b: i64| -> i64 { a + b });
vm.register_fn("sin", |x: f64| -> f64 { libm::sin(x) });
For functions that may fail, use the fallible interface.
vm.register_fn_fallible("get_setting", |key: String| -> Result<String, VmError> {
fetch(&key).map_err(|e| VmError::NativeError(format!("{}", e)))
});
Custom struct types can cross the host-guest boundary
using the KeleusmaType derive macro.
use keleusma::KeleusmaType;
#[derive(KeleusmaType, Debug, Clone)]
struct Point { x: f64, y: f64 }
vm.register_fn("midpoint", |a: Point, b: Point| -> Point {
Point { x: (a.x + b.x) / 2.0, y: (a.y + b.y) / 2.0 }
});
VM Lifecycle
The virtual machine transitions between states as it executes scripts.
let state = vm.call(&[Value::Int(input)])?;
loop {
match state {
VmState::Yielded(output) => {
let reply = host_process(output);
state = vm.resume(reply)?;
}
VmState::Reset => {
// Hot swap opportunity
state = vm.resume(next_input)?;
}
VmState::Finished(value) => {
break;
}
}
}
A Yielded state indicates
that the script has produced output
and awaits the next input.
A Reset state indicates a phase boundary
where hot code swapping is permitted.
A Finished state indicates
that the script has returned a final value.
Hot Code Swapping
Keleusma supports replacing a running script at designated reset boundaries without stopping the virtual machine. The data segment persists across the swap.
match vm.resume(input)? {
VmState::Reset => {
let new_module = recompile_or_load()?;
let initial_data = vec![Value::Int(0); slot_count];
vm.replace_module(new_module, initial_data)?;
state = vm.call(&[fresh_input])?;
}
_ => { /* continue normal execution */ }
}
Hot code swapping is useful for live development workflows. Audio engines can replace synthesis scripts without interrupting playback. Game scripting systems can reload behavior scripts without restarting the game loop.
Worst-Case Analysis
The structural verifier computes worst-case execution time and worst-case memory usage for every execution path before the virtual machine starts. If the verifier cannot prove bounded execution, the program is rejected.
Cost Model
Each bytecode instruction has an assigned integer cost. The verifier sums costs along the worst-case path through each function and each stream iteration.
Common instruction costs include the following.
| Cost | Instructions |
|---|---|
| 1 | Constants, local variable access, stack operations, control flow delimiters |
| 2 | Arithmetic, comparisons, indexing, type casts, return |
| 3 | Division, modulo, field access by name, type tests |
| 5 | Struct and enum construction, array and tuple allocation |
| 10 | Function calls, native function calls |
Arena Sizing
The host must provide an arena large enough for the worst-case memory usage. Three approaches are available.
Default capacity is 64 kilobytes.
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
Automatic computation sizes the arena to the verified worst-case memory usage.
let cap = keleusma::vm::auto_arena_capacity_for(&module, &[])?;
let arena = Arena::with_capacity(cap);
Static buffer is suitable for embedded targets without a heap allocator.
static mut ARENA_BUFFER: [u8; 16 * 1024] = [0; 16 * 1024];
let arena = unsafe {
Arena::from_static_buffer(core::ptr::addr_of_mut!(ARENA_BUFFER))
};
Compiling to Bytecode
Scripts can be compiled to bytecode files for deployment without the source.
$ keleusma compile hello.kel -o hello.kel.bin
The host application loads precompiled bytecode using zero-copy deserialization.
let bytes = std::fs::read("hello.kel.bin")?;
let mut vm = Vm::load_bytes(&bytes, &arena)?;
Precompiled bytecode is useful in embedded deployments where source compilation at runtime is undesirable or impossible. The bytecode format uses rkyv for zero-copy deserialization.
Conclusion
Keleusma provides a scripting language
with static guarantees about termination,
memory usage, and execution time.
The three-function taxonomy of atomic total,
non-atomic total, and productive divergent functions
ensures that the verifier can assign
bounded costs to every execution path.
The no_std + alloc design,
arena-based memory management,
and precompiled bytecode support
make the virtual machine suitable
for resource-constrained embedded targets.
The language repository contains additional example scripts demonstrating language features and Rust embedding examples demonstrating host integration patterns including worst-case memory attestation, yield-based coroutines, string handling, generics, zero-copy bytecode loading, and a full SDL3 audio piano roll with hot code swapping.
The documentation directory contains a complete language grammar specification, instruction set reference, type system documentation, execution model description, and a survey of related work in synchronous reactive languages, coalgebraic stream processing, and worst-case execution time analysis.
Future Reading
- Keleusma, Documentation Directory
- Keleusma, Language Design
- Keleusma, Grammar Specification
- Keleusma, Related Work Survey
- Rust, Embedded Rust Book
- Turner, Total Functional Programming (2004)
References
- Reference, Arena Allocator
- Reference, Bytecode
- Reference, Coroutine
- Reference, Keleusma on crates.io
- Reference, Keleusma on GitHub
- Reference, Keleusma Documentation
- Reference, Keleusma Example Scripts
- Reference, Keleusma Rust Examples
- Reference, Pattern Matching
- Reference, Productivity
- Reference, rkyv Zero-Copy Serialization
- Reference, SDL3
- Reference, Stack Machine
- Reference, Stream Processing
- Reference, Total Functional Programming
- Reference, Worst-Case Execution Time
- Reference, Zero-Copy Serialization
- Related Post, Getting Started with no_std Rust Programming
- Research, Turner (2004), Total Functional Programming
- Research, Rutten (2000), Universal Coalgebra
- Research, Wilhelm et al. (2008), WCET Survey