Design 01 - Truly Colorless Functions

A design exploration into concurrency and execution policies of Sabre

Written by Reuben Roessler@ Thu Oct 15 2026

With all the recent advancements in programming language design, I find it fascinating that many of them either opt for an asynchronous model that has some form of function coloring or they leave concurrency to userland implementations. Because of this, I wanted Sabre to feature a concurrent runtime, whilst also keeping functions truly colorless. This lofty goal eventually led me to designing execution policies, which I think are an elegant syntactic solution to this.

Note: This is a continuation (or follow up) from an original blog post I made when I first started toying with the idea of execution policies. There you will see some of my initial thoughts, mostly stemming from realizations based on concurrency in the Zig programming language. This post will instead go more in-depth into the actual design of colorless functions and concurrency in Sabre.

Researching the Past

There is no better place to start than at the beginning, so let's look back at the blog posts that ultimately shaped this journey:

These are all some incredible posts discussing the various nuances of async/await syntax and runtime design found within many programming languages. Some even try to offer solutions that ultimately end up falling short of solving the function coloring problem. I highly recommend reading through these before continuing, though they aren't strictly required to understand this design exploration.

What Color is Your Function?

This phrase, originally popularized by Bob Nystrom's infamous post proposed a colorful allegory for explaining common pitfalls with modern async/await syntax. He postures a theoretical language which has the following rules for two distinctly colored functions:

  1. Every function MUST have a color (implicit or explicit)
  2. Calling a function MUST use that colors convention
  3. You CAN call a blue function from within a red function
  4. You CANNOT call a red function from within a blue function

The result of which looks and functions surprisingly familiar to other popular languages:

red_function do_something_carnelian() {
    do_something_azure(); // No worries!
}

blue_function do_something_azure() {
    do_something_carnelian(); // ERROR!
}

Even languages that use a userland or library solution also fall short in a rather subtle way. Take the recent std.Io interface addition to Zig, which provides concurrency from a dedicated instance of the std.Io interface. That statement should already be ringing alarm bells as instead of coloring functions with the async keyword, a parameter now holds the coloring. This also extends to other languages that use thread pools to manage concurrency.

function do_something_carnelian(io: async) {
    do_something_azure(); // No worries!
}

function do_something_azure() {
    do_something_carnelian(); // ERROR!
}

Languages Without Colors

It is good to note that there are some notable programming languages that do have colorless functions. However, their solutions typically lack the capability of being extensible. Ruby is an incredible example of this, where although threads can be easily created and awaited, there is little functionality for customizing the scheduler:

colorless.ruby
# Some abitrary computation
task = proc do
    "Hello, World!"
end

# Can be invoked synchronously
puts task.call()

# Or invoked asynchronously
puts Thread.new(&task).value

Note: Since Ruby 3.0 there is the async/scheduler gem which allows setting a custom scheduler for Fiber values. This extensibility however falls short as it assigns a global scheduler for the current thread and cannot be applied manually when creating fibers.

Goalposts for Concurrency

With the above examples of function coloring we have mostly described the problem that we are trying to solve. As such, let's describe a set of core goals that we want to achieve with our solution:

  1. Functions CANNOT be annotated with async or parameter based coloring
  2. Functions MUST have the ability to be called agnostically (as sync, async or other)
  3. Syntax MUST be concise, readable and reasonably familiar
  4. Global variables CANNOT be used and must be an extendable solution
  5. Optional: Syntax SHOULD allow for method chaining and be intuitive

Back to the Future

Coming back to the implementation in Zig, the first design decision we should make use of is the Future interface. Synonymous with the term Promise that was coined in 1976 by Danial P. Friedman and David Wise, the Future type perfectly encapsulates the blocking semantics we desire with the .await() method. This led to the initial design that was incorporated into Sabre in early 2025.

// A function that has some abitrary computation
let task = fn (value) => Debug.println(value);

// Initial design with a global factory for "Future" values
let result = Future.async(task, 42).await();

This solution almost solves all our problems, with the only violation being that a global variable has been used of which the evilness of such an action is insurmountable. More correctly, the use of the global Future.async factory is not easily extendable by developers, as Sabre does not allow modifying internal namespaces.

Go-ing Against the Rules

Coming back a step, let's take a quick detour into how the Go programming language manages concurrency. Previously, I carefully omitted Go which similar to Ruby has no concept of function coloring. Additionally, Go coordinates concurrency through the use of goroutines which operate similarly to fibers in Ruby. In effect, both of these implementations work on variations of the following model:

  • Processor: Represents a logical processor such as a CPU core on a machine.
  • Thread: Represents a physically OS thread created and managed by the operating system.
  • Fiber: Represents a lightweight unit of execution that can be scheduled on a parent thread.

Fundamentally, this is the same principle that Sabre coordinates concurrency with. This use of fibers is incredibly powerful for concurrency as they can yield themselves to the scheduler whilst other tasks run. By awaiting a Future result, the current fiber would yield itself to a queue that would later be resumed after the Future is completed.

Goroutines themselves are especially inspiring, as they introduced syntax that inverts concurrency:

concurrency.go
package main

import (
    "fmt"
    "sync"
)

func task(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Print(id)
}

func main() {
    var wg sync.WaitGroup
    values := []int{1, 2, 3}
    for _, ii := range values {
        wg.Add(1)
        go task(ii, &wg)
    }
    wg.Wait()
}

By instead leveraging a prefix operator, Go shifts the paradigm of concurrency by instead creating goroutines at the desired callsite. This led me to realize that the root cause of function coloring stems from a design flaw in annotating declarations with colors. By moving these annotations to the callsite, Go was able to mostly reuse functions agnostically.

Note: Looking at the above example we can still see some issues with our implementation in Go. This being the use of a WaitGroup to maintain synchronization. This is where I see Go falling short as the perfect example for colorless functions. By relying on synchronization primitives, Go now needs to pass these across boundaries unlike the Future interface which integrates synchronization with the blocking .await() method.

Unconventional (Calling) Conventions

Taking inspiration from the goroutine syntax, I initially decided to replace the Future.async global with a prefix async operator. This however ended up breaking our beautiful chainability as we now required parenthesis around to call any Future methods. To alleviate this, I decided to move to an infix async operator instead.

// A function that has some abitrary computation
let task = fn (value) => Debug.println(value);

// Attempt using a prefix operator for execution
let result = (async task(42)).await();

This last solution almost felt perfect, but there was a lingering feeling that something was missing to make this solution more readable. Most notably, the gap between task and async stuck out to me as being incredibly disjointed. From a readability perspective, it felt as though there was no connection between the two. After some tweaking with styling, I landed on the final syntax available in Sabre today:

// A function that has some abitrary computation
let task = fn (value) => Debug.println(value);

// Final design for creating "Future" values in Sabre
let result = task::async(42).await();
              // ^^----- This is the "async" execution policy!

Taking inspiration from how C++ names std::launch parameters, I decided to name this calling convention syntax an execution policy. It also meant we could describe alternative execution policies, as seen above, for other calling conventions used in Sabre. From introducing this syntactical choice, execution policies are able to fulfill all our design requirements and provide truly colorless functions.

  • Function declarations are now colorless by design
  • Concurrency is now explicit when tied to a callsite
  • Syntax is concise, readable and reasonably familiar
  • Execution policies can be customizable for other conventions
  • Bonus: Method chaining is maintained and intuitive for developers
  • Bonus: The :: token simplifies completion analysis in language servers.

Of these goals, only extensibility is still the area that needs improvement in Sabre. In terms of syntax, execution policies can be easily extended by introducing a new policy keyword. Logistically however, introducing a spawn policy for custom scheduling poses questions with regard to interacting with other policies.

// Suppose we have a task that calls common "Future" policies and/or routines
let task = fn (duration: Number) {
    // We could invoke a delay for the context of this "Future"
    Future.delay(duration).await();

    // Or we could call another task with the "async" policy
    another::async(duration).await();
};

// If we then spawn our task with a custom scheduler, how does it interact with the other policies?
let thenable = task::spawn(scheduler, 1000);

Should the spawn policy attach the custom scheduler to the Future created and cause all subsequent calls to async to use the custom scheduler, or should it only be used in that instance? My current through process is that spawning a future with a custom scheduler should be pervasive and that all subsequent calls should use that scheduler unless specified otherwise.

Note: Currently both the addition of new execution policies and the proposed spawn policy have not yet been implemented in Sabre. This is because I am still working out the finer details with how Sabre should integrate these features in an intuitive and simplistic manner for developers that want to use them. I am also very open to ideas from the community as well on this and will use this opportunity to create a system for adding proposals for Sabre's design.

Stitching Threads Together

After much thought and jumping between multiple different designs for concurrency, Sabre is now powered by what I call execution policies. This design that I initially speculated back in 2025 is able to truly remove function coloring, whilst also enabling the capability for extensible concurrency support.

Although this started as an extremely lofty goal, I believe that this provides both an elegant solution and stronger explanation of where function coloring stems from. Looking to the future, I hope that others can utilize and improve this design to weave the final thread for their own concurrency tapestries.