Skip to content
📖 The Auto Programming LanguageBook contents

Actor Concurrency ​

This chapter opens Phase 3 of TAPL -- the stage where Auto reveals its full power as an AI Operating System. The actor model is fundamental to Auto. It is not an add-on library or an optional feature. It is baked into the language from the ground up.

In this chapter, we will learn how to write concurrent programs using Auto's built-in actor system: spawning tasks, sending messages, receiving with on, managing isolated state, and handling errors across actor boundaries.

Concurrency in Auto ​

Writing programs that do multiple things at once is one of the hardest problems in software development. Traditional languages use threads -- lightweight copies of the running process that share memory. Threads are powerful but dangerous: two threads accessing the same data at the same time can cause data races, deadlocks, and subtle bugs that appear only under heavy load.

Auto takes a fundamentally different approach: the Actor model. In the actor model, concurrency is expressed through independent actors that communicate exclusively by sending messages to each other. No actor can directly access another actor's state. This eliminates entire classes of concurrency bugs:

  • No data races -- actors do not share memory
  • No deadlocks -- there are no locks to deadlock on
  • No stale references -- communication is via message passing, not shared pointers

This model maps naturally to how an AI Operating System works. In Auto, every task, every service, every background job is an actor. They run independently, communicate through messages, and fail without bringing down the whole system.

Auto's ownership system (which we learned about in Chapter 12) still plays a crucial role here: it ensures that messages sent between actors transfer ownership cleanly, preventing the sender from accidentally using data that has already been sent away.

Note: Auto also supports traditional thread-based concurrency for interoperability with Rust libraries. This chapter focuses on the Actor model, which is the idiomatic Auto approach.

Spawning Tasks ​

The most basic unit of concurrency in Auto is a task -- a function that runs independently. You create a task with the spawn keyword, which starts the function running in the background.

Spawning a task and sending it messages

This prints:

Greeting: Hello from main!
Greeting: Auto is awesome!

Let us break down what is happening:

  1. fn greeter() -- Defines a function that will run as an actor. It enters an infinite loop (for true) waiting for messages.

  2. on msg String -- This is Auto's message receive syntax. It waits for a message of type String and binds it to the variable msg. When a message arrives, the body of the on block runs.

  3. spawn greeter -- Starts the greeter function running as an independent actor. The main program continues without waiting.

  4. send greeter "Hello from main!" -- Sends a string message to the greeter actor. The send keyword is fire-and-forget: it delivers the message and returns immediately.

The contrast with other languages is stark. In Rust, you need to create a channel with mpsc::channel(), pass the receiver into the spawned thread, and call .unwrap() on every send. In Python, you need threading.Thread and queue.Queue. In C, there is no standard message-passing primitive at all. In Auto, it is two lines: spawn and send.

Sending Messages ​

In the previous example, the greeter actor only understood String messages. Actors can receive any type of message. A common pattern is to use an enum to define the set of commands an actor accepts.

An actor with typed command messages

The key ideas here:

  1. enum Command -- Defines the protocol for what messages this actor accepts. Enums are perfect for this because they exhaustively list every possible command, and the compiler ensures you handle all of them.

  2. on cmd Command -- Receives a Command message. The body uses the is pattern matching syntax (from [Chapter 7][ch7]) to dispatch on each variant.

  3. send caller count -- The special identifier caller refers to whoever sent the current message. This is how actors send results back.

  4. send counter Command.Increment -- Messages are fully typed. The compiler ensures you can only send Command values to the counter actor.

In Rust, this requires an enum definition, a match expression, channel creation, and thread spawning. In Python, you need a separate Enum class and explicit if/elif chains. Auto collapses all of this into the on expression with pattern matching.

Receiving with on ​

The on keyword is how actors receive messages. It blocks until a message arrives, then binds the message to a variable and runs the handler body. The handler can use any pattern matching syntax we have already learned.

Receiving typed messages with `on`

This prints:

Fetched: https://auto.dev

The Request type is a custom type (from [Chapter 6][ch6]) with two fields: url and reply_to. The fetcher actor receives Request messages and processes them. In main, we construct a Request with type literal syntax and send it to the fetcher.

The on req Request syntax does two things at once: it waits for a message of type Request, and it binds the received value to the variable req. Inside the handler, you access the fields with dot notation: req.url, req.reply_to.

This pattern -- defining a message type and receiving it with on -- is the foundation of all actor communication in Auto.

Actor State ​

One of the most powerful aspects of the actor model is that each actor can maintain its own private state. Because no other actor can access this state, there is no need for locks, mutexes, or any synchronization primitive. The actor processes one message at a time, so state changes are always sequential.

An actor with isolated state

This prints:

counter: 1
counter: 2
Unknown command: hello

The stateful_actor function declares a var state before the message loop. This variable persists across messages. When the actor receives "inc", it increments state.count. When it receives an unknown command, it prints an error. The state is completely isolated -- only this actor can see or modify it.

In Rust, this pattern requires Arc<Mutex<State>> to share state across the thread boundary. In Python, you rely on the GIL or explicit locks. In Auto, the isolation is guaranteed by the language itself. No locks, no Arc, no Mutex -- just a var that lives inside the actor function.

This is what we mean when we say the actor model is "baked into the language." Auto does not need a separate library for concurrency because every function can become an actor with spawn, and every var inside that function is automatically actor-local.

main as Primary Actor ​

In Auto, main is itself an actor. It is the primary actor -- the entry point of the program and the default coordinator. This means main can also receive messages using on, just like any other actor.

`main` as the primary actor receiving messages

This prints:

Total so far: 20

This example shows a complete request-response cycle:

  1. main spawns a worker and sends it the number 10.
  2. worker receives the number in on n int, computes n * 2, and sends the result back to main using send main.
  3. main receives the result in on result int and updates total.

The send main syntax in the worker function refers to the primary actor. This is how workers report back to the coordinator. In Rust, this requires creating two channels (one for each direction) and passing the sender into the spawned thread. In Auto, it is a single expression.

This pattern -- main as the orchestrator, spawning workers and collecting results -- is one of the most common concurrency patterns in Auto. It is the foundation of fan-out, map-reduce, and pipeline architectures.

Multiple Actors ​

Real programs rarely have just one background task. The actor model truly shines when you have many actors working in parallel, each handling a different piece of the problem.

Spawning multiple workers with parameterized actors

This prints:

Worker 1 computing 42
Worker 2 computing 100
Worker 3 computing 7

Key observations:

  1. fn worker(id int) -- The worker function takes a parameter. When you spawn worker(1), Auto creates a new actor with id set to 1.

  2. spawn worker(1), spawn worker(2), spawn worker(3) -- Three independent actors, each with its own id and its own message queue.

  3. send worker(1) Job.Compute(42) -- Messages are addressed to specific actor instances. worker(1) and worker(2) are different actors, even though they run the same function.

  4. is job { Compute(n) => ..., Done => ... } -- The is expression (pattern matching from [Chapter 7][ch7]) works inside on handlers to destructure enum variants. Compute(n) extracts the inner value.

This is the fan-out pattern: distributing work across multiple actors. Each worker runs independently, processes its own messages, and maintains its own state. If one worker crashes, the others continue unaffected.

In Rust, this requires three separate channel pairs. In Python, three separate Queue objects. In Auto, each spawn automatically creates the actor's mailbox -- you never manage channels yourself.

Error Handling Across Actors ​

When actors communicate, errors need to cross actor boundaries. The actor model provides a clean way to do this: send error information as a message, just like any other data. The !T error handling from [Chapter 9][ch9] works naturally with actor message types.

Propagating errors across actor boundaries

This prints:

Processed: hello world
Error: Empty data

The processor actor validates its input. If the string is non-empty, it sends a WorkResult.Success back to main. If the string is empty, it sends a WorkResult.Error. The main actor receives the result and uses is pattern matching to handle both cases.

This pattern -- wrapping results in a success/error enum and sending them as messages -- is the actor-model equivalent of the !T (Result) type from [Chapter 9][ch9]. Errors are just data. They flow through the same message system as everything else. There are no exceptions, no panic/unwind, no try/catch. If an actor encounters a problem, it sends an error message. The caller decides what to do with it.

Summary ​

This chapter introduced the Actor model -- Auto's built-in approach to concurrency. Here is what we learned:

ConceptSyntaxPurpose
Spawn an actorspawn nameStart a function as an independent task
Send a messagesend actor valueDeliver data to an actor (fire-and-forget)
Receive a messageon var Type { }Block until a message arrives
Reply to sendersend caller valueSend a result back to whoever sent the message
Send to mainsend main valueReply to the primary actor
Parameterized spawnspawn fn(args)Create multiple actor instances from one function
Address instancessend fn(id) valueTarget a specific actor instance

Key takeaways:

  1. Actors are functions -- Any function can become an actor with spawn. The actor runs the function body in a loop, processing messages one at a time.

  2. Communication is via messages -- Actors never share memory. They communicate exclusively through send and on.

  3. State is isolated -- Variables declared inside an actor function are private to that actor. No locks are needed because only one message is processed at a time.

  4. main is an actor -- The primary actor can receive messages with on, making it a natural coordinator for worker actors.

  5. Errors are messages -- Error propagation across actors uses the same message system as normal data. No exceptions needed.

  6. The model eliminates bug classes -- No data races (no shared memory), no deadlocks (no locks), no stale references (ownership transfer).

The actor model is why Auto can serve as an AI Operating System. Every background task, every service, every AI agent is an actor. They communicate through messages, fail independently, and scale naturally across cores. In the next chapter, we will build on this foundation to explore more advanced concurrency patterns.

Released under the MIT License.