Skip to content

Wyzer comparison rollup — choreographic programming, and location-typed actor refs as the piece worth taking #1460

Description

@paul-hammant

Rollup of a comparison between Wyzer (Wyzer-Lang/Wyzer, surveyed at the in-tree snapshot ../wyzer/, HEAD 2fb83ca, 2026-08-07) and Aether, plus an assessment of which Wyzer ideas are worth pulling in. Filed for visibility / triage.

Unlike the Rux rollup (#581), this one does contain a big idea worth taking seriously: choreographic programming. Most of the rest of Wyzer is far behind us and not worth copying.

TL;DR

Wyzer is early (~5,100 LOC of OCaml, ~62 test programs, no CI, no Vec/HashMap/std::net yet) but it is built around a genuinely novel idea we have no answer to: declare the machines, write one program, let the compiler split it and insert the network calls — with location carried in the type system so cross-machine data can't be silently used locally.

The actionable takeaway is not "build choreography." It is one specific piece: location-typed values, which is a natural extension of the session-protocol work already proposed in #385.

Wyzer vs Aether — high-level

Dimension Aether Wyzer
Backend Transpiles to readable C; inherits the C optimizer; runs anywhere with a C compiler LLVM IR (lib/codegen.ml) + a tree-walking interpreter (lib/eval.ml)
Implementation C (~7k LOC compiler, ~42.5k runtime+std) OCaml (~5.1k LOC total)
Maturity Since 2025-03, 3,234 commits, real downstream users (avn, aether-ui, aeb) ~months old, 10 PRs
Memory model Manual + compiler-inserted heap tracking, defer Perceus refcounting with in-place reuse when uniquely owned
Concurrency Actors (spawn/receive/!/?), in-process Choreographic roles; std::thread not implemented yet
Defining feature Capability sandboxing, C interop/embedding, compile-to-C portability Choreography: role @Server / role @Sensor, endpoint projection, location-typed values
Stdlib 69 std + 6 contrib modules 5 working (io, math, time, process, string); fs/net/collections/thread unchecked in their TODO
Tests / CI 984 .ae programs + C unit suite; 23-job matrix (Linux/macOS/Windows/FreeBSD/RISC-V/WASM/Cortex-M4, valgrind, ASan) 62 .wyz files; no CI

The one idea worth taking: location-typed values

Wyzer's choreography rests on three parts. Only the middle one is worth lifting on its own.

  1. Role declarations + endpoint projectionrole @Server; at the top, fn get_temperature() @ Sensor, and a compiler pass (lib/projection.ml, 241 LOC) that strips the AST per role behind a --role CLI flag. Interesting, but it presumes a whole-program, single-file world that doesn't match how Aether programs are built.

  2. Location in the type system. ⭐ If the Sensor returns a u32, its type is u32@Sensor, and using it on the Server requires an explicit transfer(). Functions with no role annotation infer as Poly and are guaranteed not to touch node-specific globals. This is the part with real value independent of the rest, and it maps onto Aether directly: today an actor_ref says what messages exist, and Actor session protocols for compile-time message sequence checks #385 proposes adding when each is legal. Neither says where the actor is — so nothing stops code treating a remote actor's reply as a local value, which is precisely the bug class that bites when actors stop sharing an address space.

  3. Implicit RPC on plain function calls. Calling a @Sensor function from @Server silently becomes a network round-trip. I'd argue against copying this: it hides latency and failure behind syntax that looks free, which is the classic distributed-systems footgun (cf. Waldo's A Note on Distributed Computing). Aether's explicit ! / ? is the better default — the send is visible.

Why this fits #385, not a separate feature

#385 already proposes protocol state on an actor reference (actor_ref<FileWriter>, rejecting Write after Close). Location is the same kind of compile-time annotation on the same type:

Both are analyses over sends through typed references; both want the same escape hatches for dynamic refs and FFI. Designing them together seems much cheaper than bolting location on later, and it would give the three open distributed-actor proposals (#423 ephemeral identity, #424 downhill load balancing, #426 reliable byte streams) a type-level foundation they currently assume but don't have.

Suggested scope if pursued: a design spike only — no implementation commitment. Concretely: can an actor_ref carry a location parameter (local / a named node / any), can the compiler reject using a remote reply as a local value without an explicit materialization step, and does that stay readable next to existing spawn/!/? code. If the answer to any of those is ugly, the idea dies cheaply.

Also worth a look (smaller)

  • Perceus refcounting with in-place reuse. Wyzer gets no-GC automatic memory management and in-place mutation when the refcount is 1. Not a realistic swap for Aether's model at this stage, and it would collide with the "runtime stays C" line — but the in-place-when-unique optimization is worth knowing about as a codegen idea. Reference: lib/perceus.ml (188 LOC) is small enough to read in one sitting.
  • Compile-time-checked physical transport. role @Sensor { transport: "I2C", address: 0x48 } — declaring the hardware channel in the role and having the compiler verify it. We have nothing like this; it's a neat fit for an embedded story given we already build for Cortex-M4.

Aether already has a better version — do NOT copy

  • Backend. LLVM buys optimization for free, but compile-to-C is why we run on FreeBSD, RISC-V, WASM and Cortex-M4 from one codebase. Don't re-litigate.
  • Stdlib, FFI, testing, CI. Not close, and not the interesting part of the comparison.
  • Implicit RPC. See above — actively worse than explicit !/?.

Caveats on this survey

Assessed from Wyzer's docs, TODO.md checkboxes, and source layout — not by building or running it. TODO checkboxes can lag reality in either direction. Two specifics worth verifying before leaning on anything here: std::net is unchecked in their TODO, and "export physical standalone binaries for projected nodes" is also unchecked — so the choreography feature appears to be real in the type system and projection pass but not yet end-to-end over a real network. The idea is the valuable part, not the implementation.

Companion doc following the docs/cross-references/ convention to follow if this is worth keeping discoverable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions