# Troupe：multi-role finite state machine

**URL:** <https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063>\
**Category:** Showcase\
**Created:** [November 16, 2025, 1:12am UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063 "2025-11-16T01:12:57Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![sdzx-1](https://ziggit.dev/user_avatar/ziggit.dev/sdzx-1/32/3729_2.png) [@sdzx-1](https://ziggit.dev/u/sdzx-1)\
**Post date:** [November 16, 2025, 1:12am UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/1 "2025-11-16T01:12:57Z")

</div>

Multi-role finite state machine, used for multithreaded programs or distributed programs.

> **[GitHub - sdzx-1/troupe: Multi-role finite state machine](https://github.com/sdzx-1/troupe)**
>
> Multi-role finite state machine

The behaviors of multiple characters are orchestrated into a state machine. Imagine a theater with many characters performing a play. The behaviors of all characters are arranged as a whole in the script. When a certain message is sent, everyone will complete their performance according to the script.

Some similar libraries:

1. [Home - Choral Language Website](https://www.choral-lang.org/index.html)
2. [GitHub - gshen42/HasChor: Functional choreographic programming in Haskell](https://github.com/gshen42/HasChor)

* * *

This project was initially called “polysession,” and I only intended it for developing multi-role communication protocols.

However, I later discovered its uses extend far beyond communication protocols. For example, you can use it to create game scripts and write multi-threaded programs.

When there is only one participating role, `troupe` is equivalent to `polystate`. Or, you can consider `polystate` as a special case of `troupe`.

---

<div class="post-metadata">

**Author:** ![sdzx-1](https://ziggit.dev/user_avatar/ziggit.dev/sdzx-1/32/3729_2.png) [@sdzx-1](https://ziggit.dev/u/sdzx-1)\
**Post date:** [November 16, 2025, 1:28am UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/2 "2025-11-16T01:28:38Z")

</div>

The `troupe` allows you to construct the overall control flow of your program through declarative composition and generates the overall control flow diagram through the compiler.

> <https://github.com/sdzx-1/troupe/blob/74b00bdaf8e62d599429c1fe7a8b823683762439/examples/random_pingpong_2pc.zig#L145-L153>

For example, the above definition will generate the following control chart:

 ![t](https://ziggit.dev/uploads/default/original/2X/7/7e46065d184c03b84820d4b77d11c336b44fa372.png)

The effect during runtime:  
 ![GIF 2025-11-16 09-27-54](https://ziggit.dev/uploads/default/original/2X/0/0c9e52e5b0c6eecd4bda99585135d1a7f6aadc32.gif)

---

<div class="post-metadata">

**Author:** ![tholmes](https://ziggit.dev/user_avatar/ziggit.dev/tholmes/32/5641_2.png) [@tholmes](https://ziggit.dev/u/tholmes)\
**Post date:** [November 17, 2025, 8:09pm UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/3 "2025-11-17T20:09:02Z")

</div>

I don’t understand it at all, but from your descriptions it seems like something sublime

---

<div class="post-metadata">

**Author:** ![sdzx-1](https://ziggit.dev/user_avatar/ziggit.dev/sdzx-1/32/3729_2.png) [@sdzx-1](https://ziggit.dev/u/sdzx-1)\
**Post date:** [February 10, 2026, 8:55pm UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/4 "2026-02-10T20:55:13Z")

</div>

> **[IoUring: update to new Io APIs](https://codeberg.org/ziglang/zig/pulls/31158)**
>
> zig - General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.

The stacked coroutines have been merged into master.

A troupe is a framework describing multi-role communication. Using `evented`, we’ll use one million pairs of pingpong protocols, representing two million `fibers`.

Each pair of `fibers` in the pingpong protocol will send messages to each other 30 times.

> <https://github.com/sdzx-1/troupe/blob/b76da867a991714d3e0c3c2632b0a2d338bc2797/examples/pingpong.zig#L31>

```zig
zig build pingpong --release=fast

```

It took about 10 seconds to complete on my PC.

ps: I set the value of A here to 1 \* 1024.

> **[6147 lines 219 KiB Zig Raw Blame History - zig/lib/std/Io/IoUring.zig at...](https://codeberg.org/ziglang/zig/src/commit/e314dadb015a913a2cdc12298986e9181305d8db/lib/std/Io/IoUring.zig#L244)**
>
> zig - General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.

---

<div class="post-metadata">

**Author:** ![invlpg](https://ziggit.dev/letter_avatar_proxy/v4/letter/i/13edae/32.png) [@invlpg](https://ziggit.dev/u/invlpg)\
**Post date:** [February 10, 2026, 10:03pm UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/5 "2026-02-10T22:03:53Z")

</div>

> [@sdzx-1](#):
>
> The stacked coroutines have been merged into master.

It doesn’t have networking yet, but for anything needing concurrency for local IO, it seems to be there and working reasonably okay.

Unless you’re debugging `IoUring.zig` itself, I’d recommend adding the following to your `root` module as it currently has some quite verbose `debug` logging.

```zig
const std_options: std.Options = .{
    .log_scope_levels = &.{ .{ .scope = .@"io-uring", .level = .info } },
};

```

Not the author of it, just someone who has been following development closely and been toying around with it since the PR was made a little while ago.

---

<div class="post-metadata">

**Author:** ![sdzx-1](https://ziggit.dev/user_avatar/ziggit.dev/sdzx-1/32/3729_2.png) [@sdzx-1](https://ziggit.dev/u/sdzx-1)\
**Post date:** [February 17, 2026, 12:09pm UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/7 "2026-02-17T12:09:29Z")

</div>

I updated the README.md so that more people can recognize the value of this library.

---

<div class="post-metadata">

**Author:** ![sdzx-1](https://ziggit.dev/user_avatar/ziggit.dev/sdzx-1/32/3729_2.png) [@sdzx-1](https://ziggit.dev/u/sdzx-1)\
**Post date:** [May 8, 2026, 2:13am UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/8 "2026-05-08T02:13:35Z")

</div>

**An example written using troupe: a file transfer protocol with block hash checking.**

The protocol defined: [troupe/examples/protocols/sendfile.zig at main · sdzx-1/troupe · GitHub](https://github.com/sdzx-1/troupe/blob/main/examples/protocols/sendfile.zig)

The main entry point: [troupe/examples/sendfile.zig at main · sdzx-1/troupe · GitHub](https://github.com/sdzx-1/troupe/blob/main/examples/sendfile.zig)

Alice sends a file to Bob over a TCP connection. Data is streamed in 4 KB chunks. After every 20 MB of data, or when the file ends, the sender sends a hash of the transmitted data; the receiver independently computes the hash and reports whether it matches, enabling early detection of corruption.

This example demonstrates real-world protocol design with Troupe:

- **Self-looping state for streaming** : `Send.send: Data([]const u8, @This())` — the `Send` state references itself, forming a cycle in the state graph that supports arbitrary-length data transfer. This is the pattern for any streaming protocol.

- **State template as protocol subroutine** : `CheckHash(A, B)` is not a single fixed state but a **parameterized state template**. It accepts two type parameters — the success continuation `A` and the failure continuation `B` — and is instantiated twice with different continuations: `CheckHash(@This(), Failed)` for periodic checkpoints (continue sending on success), and `CheckHash(Successed, Failed)` for the final chunk (exit on success).

- **Receiver-driven integrity verification** : The sender commits to a hash; the receiver independently computes the hash and reports the result. The `CheckHash` state reverses sender/receiver roles: the verification result flows from receiver back to sender.

- **Multi-state exit semantics** : `CheckHash` has two branches (`Successed` / `Failed`), each connecting to a different continuation path. This satisfies the branch notification rule: since both roles are internal, `receiver.len` must be 1.

- **TCP StreamChannel** : Unlike the in-memory channel used in other examples, sendfile runs over real TCP sockets, demonstrating that the channel abstraction is transparent to protocol logic. The same protocol definition works with any channel implementation.

[![sendfile](https://github.com/sdzx-1/troupe/raw/main/data/sendfile.svg)](https://github.com/sdzx-1/troupe/blob/main/data/sendfile.svg)

**A closer look at the `Send` state** — the most insightful part of this protocol is its type definition:

```zig
pub const Send = union(enum) {
    send : Data([]const u8 , @This()),
    check : Data(u64 , CheckHash(@This(), Failed)),
    final : Data(struct {str: []const u8, hash: u64,}, CheckHash(Successed, Failed)),
};

```

Three branches, three different continuations — the entire transmission strategy is encoded in these three lines:

- **`.send → @This()`**: Transmit a chunk, then loop back to the same state. The self-reference creates an implicit `while` loop in the state graph, enabling streaming without a dedicated loop construct.

- **`.check → CheckHash(@This(), Failed)`**: At batch boundaries, pause transmission to verify integrity. The continuation `@This()` (i.e., `Send`) is the success path — pass verification and resume streaming. `Failed` is the abort path.

- **`.final → CheckHash(Successed, Failed)`**: End of file. Send the last chunk with its cumulative hash. Both paths lead to termination — `CheckHash` here uses `Successed` and `Failed` as distinct exit routes rather than a return to `Send`.

This is the **same `CheckHash` template invoked with different continuations**. The verification logic is written once; only the “where to go next” differs between the two call sites. The `process` function that decides which branch to take is equally compact — it reads a chunk from the file, then routes based on `send_size >= batch_size` and whether the read reached end-of-file:

```zig
pub fn process(parent_ctx: *@field(context, @tagName(sender))) !@This() {
    const ctx = sender_ctxFromParent(parent_ctx);
    if (ctx.send_size >= batch_size) {
        ctx.send_size = 0;
        const curr_hash = ctx.hasher.final();
        ctx.hasher = std.hash.XxHash3.init(0);
        return .{ .check = .{ .data = curr_hash } };
    }

    const n = try ctx.reader.readSliceShort(&ctx.send_buff);

    if (n < ctx.send_buff.len) {
        ctx.hasher.update(ctx.send_buff[0..n]);
        ctx.send_size += ctx.send_buff.len;
        return .{ .final = .{ .data = .{ .str = ctx.send_buff[0..n], .hash = ctx.hasher.final() } } };
    } else {
        ctx.hasher.update(&ctx.send_buff);
        ctx.send_size += ctx.send_buff.len;
        return .{ .send = .{ .data = &ctx.send_buff } };
    }
}

```

The `Send` state demonstrates a principle that recurs throughout well-designed Troupe protocols: **the type signature tells the structural story; the handler function fills in the runtime details.** Reading the three union fields, you already know the entire flow — streaming, checkpointing, termination. The `process` function is just the concrete filling of that skeleton.

---

<div class="post-metadata">

**Author:** ![sdzx-1](https://ziggit.dev/user_avatar/ziggit.dev/sdzx-1/32/3729_2.png) [@sdzx-1](https://ziggit.dev/u/sdzx-1)\
**Post date:** [July 10, 2026, 1:15am UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/9 "2026-07-10T01:15:09Z")

</div>

> **[GitHub - sdzx-1/polyrole-cs: client-server](https://github.com/sdzx-1/polyrole-cs)**
>
> client-server

Focus on client-server communication and add more engineering capabilities.

---

<div class="post-metadata">

**Author:** ![sdzx-1](https://ziggit.dev/user_avatar/ziggit.dev/sdzx-1/32/3729_2.png) [@sdzx-1](https://ziggit.dev/u/sdzx-1)\
**Post date:** [August 8, 2026, 10:09am UTC](https://ziggit.dev/t/troupe-multi-role-finite-state-machine/13063/10 "2026-08-08T10:09:03Z")

</div>

> <https://github.com/sdzx-1/polyrole-cs/blob/main/README_EN.md>
