Designing Concurrency Without Betraying the Rest

Concurrency wasn't added because languages are supposed to have it — it was forced by a wall hit while building something real: two forever-loops that couldn't share one process. The hard part was adding it without giving up the guarantees, and the bet is that the machinery already built for memory and effects — ownership, move, Trivial — is exactly what safe concurrency needs, extended to the thread boundary.

mereconcurrencyspawnchannelslanguage-design

A public, working language faces a question the private one could defer: what does it grow next? For Mere the answer wasn’t chosen from a wish list — it was forced. In building a real program, a wall appeared, and getting past it meant adding one of the hardest features a language with strong guarantees can take on: concurrency. This part is about adding it without betraying everything the earlier parts built.

The wall that demanded it

The pain point was concrete and unglamorous. An HTTP server is a forever-loop — it waits for requests and never returns. A program that also needs a second forever-loop — a subscriber listening to a message channel, a background worker, a scheduled job — has two loops that must run at once, and Mere had no way to run two loops in one process. The immediate workaround was to push the second loop out into the JavaScript host, letting its event loop interleave the two. That works, but it is an admission: the language couldn’t express something real programs routinely need, so the need leaked out into the runtime around it.

That is the dogfood signal in its purest form — not a feature imagined in the abstract, but a specific thing you could not write, discovered by trying to write it. Concurrency went onto the agenda because a program hit a wall, which is a very different justification from “languages have threads.” The shape recurs — server plus worker, server plus cron, server plus telemetry — so it was worth solving properly rather than routing around forever.

The hard part: keep the guarantees

Concurrency is the classic place where a language’s safety promises quietly expire. The moment two threads share mutable state, you inherit data races — bugs that depend on timing, invisible in the source, absent from most test runs. A language that spent five parts making memory and effects explicit and checkable cannot then add threads and shrug about what they share. The whole difficulty of this part is that concurrency has to be as explicit and as checked as everything else.

The bet Mere makes is that it does not need a new safety regime to get there, because the machinery it already has is the right machinery. Ownership and move already model “this value now belongs to someone else, and you can’t touch it.” The borrow annotations already distinguish shared-read from exclusive-write. The Trivial constraint already classifies what can be handled without cleanup. Safe concurrency, the bet goes, is those same ideas carried across the thread boundary: a value handed to another thread is a value moved; a value two threads share must prove it is safe to share, exactly as the type system already reasons about sharing within one thread. Concurrency becomes an extension of the ownership story, not a bolt-on beside it.

Five questions instead of one

“Add concurrency” is too vague to decide. So it was decomposed, in the recurring method of the series, into five precise questions that can each be answered:

  • The execution model — real OS threads, lightweight green threads, or async tasks?
  • Crossing the boundary — when a capability goes to a child thread, is it moved, shared, or copied?
  • Channels — what must be true of a type to send it down a channel?
  • Send and Sync — do we need trait-like markers for “safe to move between threads” and “safe to share,” and if so, how heavy?
  • Interaction with regions and with — does a child thread get its own region, and what happens to cleanup order?

And the design was pinned down the way the effect system was, by writing the programs it should accept and the ones it must reject: parallel computation that works; a logger moved into a child so the parent can no longer use it (and re-use is a compile error); a logger explicitly shared and usable from both; a single-owner network connection that is a type error to hand across; only sendable types allowed down a channel. The acceptance criteria are programs, not prose.

The narrowing

The answers, narrowed on paper and then checked with a small trial before any real implementation, all leaned toward the smallest thing consistent with the existing design.

The execution model is real OS threads — concretely, the parallelism primitive the host language, OCaml, already provides. It is the smallest implementation, it uses actual CPU cores directly, and it needs no bespoke scheduler. The async-task model, elegant as it is elsewhere, was rejected for running against the grain of the capability-passing decision — it reintroduces exactly the implicit control flow the effect system exists to remove. Choosing OS threads is the why-OCaml bet paying off once more: the host’s threads become Mere’s threads.

Crossing the boundary is move by default — a capability handed to a child belongs to the child, and the parent using it afterward is a compile-time error — with explicit sharing only for values that carry their own internal locking. A channel requires its element type to be safe to send. And the “safe to send” and “safe to share” properties are handled not by building a full trait system but by adding two more structural predicates — a Send and a Sync — alongside the Trivial and cleanup-needing distinctions the language already tracked. It is the same restraint as the rest of the project: the minimum mechanism that answers the question, reusing what exists rather than inventing a general machine. Finally, a spawned child gets a fresh region of its own, so parent and child memory don’t alias and the region model’s guarantees survive the thread boundary intact.

Nothing here is finished yet — this is the shape, narrowed and trial-checked, not the implementation. But the shape is the important commitment: concurrency in Mere is ownership extended, not safety abandoned. The next episodes build it. First, the two predicates the whole design rests on need to actually work — and getting them right turned out to have a subtle hole. Next: the Send and Sync types.

← Back to Mere: Building a Language