GitHub - tracyspacy/spacydo: SPACYDO is a stack-based virtual machine with image-based persistence. The core abstraction is a TASK: a persistent entity carrying its own executable bytecode alongside its state. Tasks can observe and modify fields of other tasks and their own.

4 min read Original article ↗

"I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages." — Alan Kay


SPACYDO is a stack-based virtual machine with image-based persistence. The core abstraction is a TASK: a persistent entity carrying its own executable bytecode alongside its state. Tasks can observe and modify fields of other tasks and their own.

VM TYPES

All stack values are 64-bit (u64) Nan-Boxed values encoding 6 distinct types:

Type Description
Null absence of value
TRUE_VAL, FALSE_VAL boolean true / false
U32 unsigned 32-bit integer
String reference into StringPool
CallData reference into InstructionsPool
MemSlice offset (25-bit) + size (25-bit) into scratch memory

See values.rs

VM INSTRUCTIONS

Type Instructions
Stack Operations PUSH_U32, PUSH_STRING, PUSH_CALLDATA, PUSH_STATE, PUSH_MAX_STATES, DUP, SWAP, DROP
Task Operations T_CREATE, T_GET_FIELD, T_SET_FIELD, T_DELETE
Storage Operations S_SAVE, S_LEN
Memory Operations M_SLICE, M_STORE
Control Flow DO, LOOP, LOOP_INDEX, CALL, END_CALL, IF..THEN
Logic operations EQ, NEQ, LT, GT

Instruction set with description is here: opcodes.rs VM instruction set is intentionally minimal and aiming to remain minimal in a future. While bytecode instructions are expressive enough already, bytecode is verbose, so new instructions should be added.

What this enables

Because behavior lives inside the task rather than in application code, programs therefore consist of networks of interacting tasks whose behavior emerges from state transitions, and this behavior is updatable at runtime without application recompilation.

The same primitive naturally expresses:

Workflows — tasks that create, modify, chain, or destroy other tasks on state change. Behavior loaded from TOML at runtime, no recompilation required.

State machines — a task whose instructions encode its own transition rules. The traffic light example is a single task: instructions define red→green→yellow→red, no external FSM framework needed.

Circuitry simulation — the BCD decoder example implements a full Binary Coded Decimal decoder from Petzold's Code. Every NOT gate and AND gate is a persistent task with its own instructions that reads input task states and updates its own.

Examples

Example What it demonstrates
examples/todo Programmable tasks — chain, hide, self-destruct, conditional completion
examples/traffic_light Finite State machine as a single self-transitioning task
examples/bcd-decoder BCD decoder circuitry from "Code" by Charles Petzold book emulation with logic gates as Tasks

Various Usage examples:

let ops = "PUSH_U32 2 PUSH_U32 2 EQ IF PUSH_U32 3 THEN PUSH_U32 4 PUSH_STRING HELLO PUSH_U32 42 PUSH_U32 42 EQ PUSH_CALLDATA [ PUSH_U32 11 END_CALL ]";
// inits vm with instructions
let mut vm = VM::init(ops)?;

// executes instructions and returns raw stack with NaN-boxed values
let raw_stack = vm.run()?;


// unboxing raw values returns: 
// [U32(3), U32(4), String("HELLO"), Bool(true), CallData("PUSH_U32 11 END_CALL")]
let unboxed_stack = vm.unbox(&stack).collect::<VMResult<Vec<_>>>()?;

// returns 3u32
let _val:u32 = unboxed_stack[0].as_u32()?;

// returns "PUSH_U32 11 END_CALL"
let _calldata:&str = unboxed_stack[4].as_calldata()?;

// example with memory write
let ops_mem =
        "PUSH_U32 0 PUSH_U32 5 M_SLICE PUSH_U32 5 PUSH_U32 0 DO LOOP_INDEX LOOP_INDEX M_STORE LOOP";

let mut vm = VM::init(ops_mem)?;
let raw_stack = vm.run()?;

// get memslice offset and size (offset,size) = (0,5,)
let (offset, size) = vm.unbox(&raw_stack).next().unwrap()?.as_mem_slice()?;

// get values from memory : [0, 1, 2, 3, 4]
let memory_values: Vec<u32> = vm
        .return_memory(offset, size)
        .map(|r| r.unwrap().as_u32().unwrap())
        .collect();


// example with memory write containing Null values
let ops_mem_null_vals = "PUSH_U32 0 PUSH_U32 5 M_SLICE PUSH_U32 1 PUSH_U32 1 M_STORE PUSH_U32 3 PUSH_U32 3 M_STORE";

let mut vm = VM::init(ops_mem)?;
let raw_stack = vm.run()?;

// get memslice offset and size (offset,size) = (0,5,)
let (offset, size) = vm.unbox(&raw_stack).next().unwrap()?.as_mem_slice()?;

// returns  vec![Return::Null, Return::U32(1), Return::Null, Return::U32(3),]
let memory = vm.return_memory(offset, size).collect::<VMResult<Vec<_>>>()?; 

// returns [1,3]
let filtered: Vec<u32> = vm.return_memory(offset, size).filter_map(|r| match r.unwrap() {
            Return::U32(val) => Some(val),
            _ => None,
        })
        .collect();

Current Scope / Known Issues:

  • storage and VM are not thread-safe
  • instruction set is not yet stabilized
  • task model is not yet stabilized
  • nested loops limited to 2 levels
  • nested calls limited to 2 frames
  • type checking during assembly to bytecode should be improved