Interaction Trees
AVM programs are interaction trees — data structures that describe a
computation interleaved with observable effects. They are the Rust encoding
of the coinductive ITree from the Agda specification.
Structure
enum ITree<E, A> {
Ret(A), // computation finished with result A
Tau(Box<ITree>), // silent step (no effect)
Vis(E, Box<dyn FnOnce(Val) -> ITree>), // emit event E, continue with response
}
Ret(a)— terminal node. The program has produced a value.Tau(next)— an internal step with no observable effect. The interpreter eliminates these iteratively.Vis(event, continuation)— the program emits anevent(an instruction) and waits for a response from the environment. Thecontinuationis a function that takes the response and produces the next tree.
Building programs
Use trigger to lift an instruction into a single-step tree:
let tree = trigger(instruction::get_self());
// tree = Vis(GetSelf, |response| Ret(response))
Use bind (or the avm_do! macro) to sequence steps:
let program = avm_do! {
let self_id <- trigger(instruction::get_self());
let input <- trigger(instruction::get_input());
ret(Val::pair(self_id, input))
};
The avm_do! macro supports:
let x <- expr;— monadic bind (expr must produce anITree)let x = expr;— pure variable bindingexpr;— sequence (discard result)expr— final expression (must be anITree)
Example tree
The following diagram shows a small program that opens a transaction, creates
two objects, commits, and then calls one of them. Each Vis node emits an
instruction and receives a typed response before continuing.
graph TD
A[Vis: begin_tx] -->|TxRef| B[Vis: create_obj]
B -->|ObjectRef| C[Vis: create_obj]
C -->|ObjectRef| D[Vis: commit_tx]
D -->|Bool| E[Vis: call]
E -->|Result| F[Ret: value]
Execution
The interpreter runs the tree iteratively:
Ret(a)— return the resultTau(next)— step tonext(loop, no recursion)Vis(instr, cont)— execute the instruction, feed the result tocont
This design avoids stack overflow for arbitrarily long programs.