Async/await inference in Firefly

Async/await inference in Firefly
Two representative examples, where the calls that may be async are underlined.

Firefly is a new general purpose programming language that tries to achieve convenience and safety at the same time, by using pervasive dependency injection. There is no global access to the file system, the network, other processes or devices. Instead, you access these through a system object that is passed to the main function, which in turn can pass this object to other methods. The idea is to give the programmer fine grained control over which parts of the code can access what (Log4Shell anybody?), without introducing monads or other explicit effect tracking.

As it turns out, this mechanism enables colorless async/await.

Because all I/O is accessed via dependency injection, a top level function call can only be asynchronous if it's called with an asynchronous argument. This leads to the first rule of inferring async/await in Firefly:

Rule 1: Top level function calls can only be asynchronous if they're called with asynchronous arguments. Every top level function gets a sync version and an async version. If one of the arguments is async, the async version is called; otherwise the sync version is called.

Inner functions may also capture async objects and call them. However, that can only come, directly or indirectly, from the arguments of the top level function. This leads to the second rule.

Rule 2: Inner functions are async if they call an async function. Such functions always come, directly or indirectly, from the arguments of the top level method.

When you call a function with multiple arguments, the function may update one argument with the the capabilities of the other though mutation. This also applies to the return value, which may be e.g. a lambda functions that updates one of the captured arguments when it's later called, possibly based on the concrete arguments given when calling the lambda function.

Rule 3: All arguments and the return values are potentially async if any of the arguments are async. The return value may later cause the arguments to become async.

Using an effect type inference based on these rules, Firefly automatically infers which calls are definitely synchronous, and which calls are possibly asynchronous. It's colorless: Programmers never have to annotate functions as async, and never have to manually  await. In fact, Firefly has no syntax for specifying such things.

Let's take a final look at the output of the static analysis:

The calls that may be async are automatically identified. This means you can write straightforward code that looks like it's blocking, and still get all the benefits of asynchronous I/O.

In part 2, we infer async/await on a concrete code example and see how it works under the hood:

Async/await inference in Firefly: Part 2
In the last post, we sketched out the high level rules of the async/await inference. This post will show how it works under the hood for a concrete example. If you missed the last post, here it is: Async/await inference in FireflyFirefly is a new general purpose programming