The Case for Inverse Coloring (Async by Default)

Status: Draft

Introduction: The Era of Latency

For the last 30 years, systems programming has operated on a lie.

The lie was that execution is immediate. When C was designed in the 1970s, "I/O" meant reading a file from a local disk. It was fast enough to block the CPU. Today, "I/O" means a network round-trip to a microservice across the continent, a database query, or a GPU kernel launch.

In the modern world, latency is the rule, not the exception. Yet, our languages still default to sync—blocking the thread, wasting resources, and forcing us to use complex syntax (async, .await, Future) just to handle the reality of modern hardware.

It is time to acknowledge that the default state of software is distributed and asynchronous. It is time to invert the model.

The Problem: The "Function Coloring" Tax

In current Rust, we have split the ecosystem in two:

  1. Sync Code: Simple, readable, but cannot scale I/O.
  2. Async Code: Scalable, but verbose and viral.

The "Function Coloring" problem is fundamentally a Polymorphism problem. If you write a generic map function in sync style, it cannot be used with an async closure. You are forced to duplicate logic. We are placing the burden of "machine details" (state machine lowering vs. C-stack usage) on the human developer, rather than the compiler.

The Proposal: "Direct Style" via Effect Polymorphism

I propose a new paradigm for our next-generation systems language: Async by Default.

In this language:

  • fn is Effect-Polymorphic: Every function is implicitly generic over its "async-ness." The syntax uses Direct Style (Implicit Await). If you call a function, you get the result, not a Future. Suspension happens transparently at the call site.
  • sync fn is a Constraint: You only type sync when you explicitly need to restrict execution to the standard C-stack (e.g., for FFI exports, ISRs, or atomic critical sections). A sync fn cannot call a standard fn (because it might suspend), effectively inverting the current coloring rules.
  • Concurrency is Structured: Because Direct Style implies sequential execution, concurrency is strictly opt-in via structured primitives (e.g., async let or zip(a, b)). This prevents the "detached task" hazards common in other models.

Why this changes everything:

1. Composition is King You can call any function from any other function. The "colors" disappear from the user's mental model. Logic flows naturally. The constraints (sync) are pushed to the edges of the graph (FFI, hardware boundaries) where they belong.

2. Safety via Effect Analysis We are not hiding the costs; we are tracking them. A major criticism of implicit await is that hidden suspension causes bugs (e.g., holding a Mutex across an I/O call). In this language, Async is a tracked Effect. The compiler proves safety. If you try to hold a std::sync::Mutex (a thread-blocking lock) across a suspension point, the compiler emits an error. You are forced to use an AsyncMutex. We gain the ergonomics of Go with the safety guarantees of Rust.

The "Zero-Cost" Defense

I know the fear: "If everything is a state machine, aren't we bloating the binary and destroying instruction cache locality?"

No. We achieve this through Effect Monomorphization.

  1. Context-Sensitive Compilation: If a function chain A -> B -> C is analyzed, and the compiler proves C never actually performs I/O, the compiler instantiates a standard, synchronous, stack-based version of the chain. It is indistinguishable from C.
  2. Pay-as-you-go State Machines: The "State Machine" transformation (spilling stack variables to a struct) is only materialized for the specific call-graph paths that actually suspend.
  3. Fast-Path Optimization: Even in async functions, code executes on the hardware stack between suspension points. If a network call returns Ready immediately (e.g., buffered data), we never allocate a task or yield to the scheduler.

Conclusion

Rust to empower everyone to build reliable and efficient software. But "reliable" now means "resilient to latency."

By making async the default, we align the language with the hardware. We use the compiler's power (monomorphization) to bridge the gap between human ergonomics and machine performance.

This is not just syntax sugar. This is acknowledging that in 2026, Waiting is the new Computing.

1 Like

I think this is the challenging part of your proposal. I think there is a lot of code where it is useful that suspension is explicit. Your proposal might work for things like mutexes, but cancellation safety (already arguably a pitfall in today’s Rust) would be even harder to track.

5 Likes

This is maybe less important.

If I have an indirection, like using a trait object, how would the optimisation for sync calls happen? Never? By an annotation?

As a vocal opponent of the current Rust async system, I long argued that "async targets" would've been a better approach. Arguably, async APIs provide an alternative way of interacting with OS (including a way of spawning parallel work in the form of threads/tasks) and thus should be treated as separate targets.

Such targets would provide a global switch for how we handle concurrency, without async/await syntax noise, traits duplication, and the ugly ecosystem split (well, there still will be some split, but on the level of Linux/Windows/etc.-specific crates). It also would allow us to have default executors in std without additional costs for non-async targets.

There are some niche scenarios which are handled poorly by this paradigm such as mixing async and non-async workloads, but it should be sufficient for 99+% of async uses in practice. Personally, I consider them similar to mixing GLIBC and MUSL in one program. Note that bare metal can handled fine in this paradigm using custom targets.

Well, first you should wonder how trait object are even implemented at all in an async-first world. Right now async fns in trait are not object safe.

And uh. Where does the runtime live? What are the details of how it runs? Does it work in dylibs?

Could you at least try to think about basic details instead of using an LLM to give the illusion of effort?

10 Likes

Can you have several runtimes doing different things at once? Can you have async runtimes that don't do IO at all?

Can you please share the prompt you used to generate this text?

4 Likes

Such design could be interesting for a scripting language or a VM/JIT implementation.

It's inappropriate for a low-level systems programming language like Rust where having low overhead, predictable code generation, and low-level control is important.

4 Likes