Actors

Actors in Kodo combine state and message handlers into a single unit. An actor holds mutable fields on the heap and exposes handler functions that can be invoked as messages through the cooperative scheduler.

The Actor Model

An actor is declared with the actor keyword. It contains typed fields (the state) and handler functions that operate on that state:

actor Counter {
    count: Int

    fn increment(self) -> Int {
        return self.count + 1
    }
}

The self parameter gives the handler access to the actor’s fields. Fields are read with self.field syntax.

Creating Actors

Create an actor by providing initial values for all fields, similar to struct literals:

let c: Counter = Counter { count: 0 }

Under the hood, the runtime allocates the actor’s state on the heap via kodo_actor_new. The state buffer is zero-initialized and then populated with the field values you provide. Each field occupies 8 bytes, accessed by offset.

Reading Fields

You can read an actor’s fields using dot notation:

let c: Counter = Counter { count: 42 }
let v: Int = c.count
print_int(v)  // 42

Field access compiles to a kodo_actor_get_field call with the appropriate byte offset.

Sending Messages

Calling a handler on an actor queues a message that the cooperative scheduler processes:

let c: Counter = Counter { count: 10 }
c.increment(5)

At the runtime level, c.increment(5) translates to a kodo_actor_send call. The handler function pointer and argument are packed into an environment buffer and enqueued as a task. The scheduler runs these tasks after main returns, just like spawn blocks.

This means handler calls are asynchronous — the call returns immediately and the handler executes later.

Actor Lifecycle

  1. Creation: Counter { count: 0 } allocates heap state via kodo_actor_new.
  2. Field access: c.count reads from the heap buffer via kodo_actor_get_field.
  3. Field mutation: Setting a field uses kodo_actor_set_field at the given offset.
  4. Message sending: c.handler(arg) enqueues a task via kodo_actor_send.
  5. Execution: The scheduler drains the task queue, running each handler in order.
  6. Cleanup: kodo_actor_free releases the heap buffer when the actor is no longer needed.

Complete Example

module actors {
    meta {
        purpose: "Demonstrate actors with state and message passing",
        version: "0.1.0",
        author: "Kodo Team"
    }

    actor Counter {
        count: Int

        fn increment(self) -> Int {
            return self.count + 1
        }
    }

    fn main() -> Int {
        // Create a Counter with initial state (heap-allocated).
        let c: Counter = Counter { count: 42 }

        // Read a field directly.
        let v: Int = c.count
        print_int(v)

        return 0
    }
}

Compile and run:

cargo run -p kodoc -- build actors.ko -o actors
./actors

Output: 42

Cooperative Scheduling

Currently, actors use the same cooperative scheduler as spawn blocks. Messages are processed sequentially after main returns. This means:

  • There is no true concurrent mailbox — messages execute one at a time.
  • Message ordering is deterministic (FIFO within the task queue).
  • An actor’s state is never accessed by two handlers simultaneously.

Future versions of Kodo will introduce asynchronous mailboxes and parallel execution, but the actor declaration syntax and semantics will remain the same.

Current Limitations

  • Handler signatures: Handlers take self as the first parameter. Additional parameters are passed as part of the message when using kodo_actor_send.
  • Handler parameters: Only Int, Bool, and String types are currently supported as handler parameters due to environment serialization constraints.
  • Field mutation: Fields can be mutated from within handlers via kodo_actor_set_field. Direct mutation from outside the actor (e.g., c.count = 10) is not supported — use a handler instead.
  • No concurrent mailboxes: Messages execute sequentially through the cooperative scheduler. True concurrent actor mailboxes are planned.

Actors vs Structs

Actors and structs both hold fields, but they differ in important ways:

FeatureStructActor
StorageStack or struct-returnHeap-allocated
Field accessDirect memorykodo_actor_get_field / kodo_actor_set_field
MethodsSynchronous callsAsynchronous messages via scheduler
MutabilityFollows ownership rulesMutable through handlers

Use structs for plain data. Use actors when you need mutable state combined with asynchronous message handling.

Next Steps