This is a documentation file for developers.
This project requires the following tools:
Install lefthook:
lefthook install
To release a new version to LuaRocks, do the following:
- If you’ve changed available source files, update the
build.modules
field in the Rockspec file. - Update the
version
field in the Rockspec file. - Update the name of the Rockspec file to reflect the new version.
- Upload a rock with the following command:
luarocks upload ./coop.nvim-$VERSION-$REVISION.rockspec --api_key=$API_KEY
A coroutine function is a Lua function, which may call coroutine.yield
.
A coroutine function must always be executed within a coroutine.
A plain coroutine function is a coroutine function that doesn’t implement any protocol for its yields. A plain coroutine function doesn’t return any values in yields nor expect any values from them.
The project overall had the following design guidelines:
- Extend coroutines without replacing them. Keep things simple by extending how coroutines work and keeping as much of their behaviour as reasonable.
- Make asynchronous code behave similarly to synchronous code.
This subsection is about the decision to implement the task
interface that
replaces coroutine
instead of using pure coroutines.
You can’t build awaiting primitives with pure coroutines. The proof would look something like this: Lua is single-threaded, so something needs to wake and run waiting threads. That means that someone needs keep a list of waiters. You can’t store such data in pure coroutines.
The awaiting feature requires bundling in a waiting queue (a future) together with a thread.
I want the framework to treat coroutine functions almost like regular functions
and have the capability to wait for results of a parallelized operation with
futures.
A (coroutine) function can return in two ways: return values or throw an error.
The error is only caught by whoever calls coroutine.resume
, because we can’t
use pcall
with coroutine functions.
That would mean that sometimes the error would get caught by the UV thread and
get lost as I can’t change how the UV thread works.
A lost error means that we would end up with a dangling future.
I decided that the future interface would be better if it was total, i.e.,
future always finishes when its coroutine is dead.
To achieve that I concluded that a small error-catching wrapper on top of
coroutine.resume
(called “task”) would do the trick and the cost of this is
worth it: the implementation is dead simple in the end.
In summary, pure coroutines lack the ability to store their results. Tasks add that useful capability at a low cost.
I decided that a future should expose a single await function that can work in three modes:
- an asynchronous task function
- a callback-based function
- busy waiting
All three cases are useful in practice and a single function makes the interface more fluent and more elegant. I just found that having three different names was clumsy.
I made await
available under a function call, so that people can use
awaitables as if they were task functions.
This is inline with the design goal to avoid asynchronous boilerplate.
await
rethrows errors. This makes await
behave like a regular function would.
I decided that Task:cancel
sets a cancelled
flag that, if intercepted,
needs to be cleared by the programmer.
This makes the cancellation interface more flexible:
- The programmer can intercept cancellation, do some clean up logic, and still proceed with cancellation.
- The programmer can now more reliably check which task was cancelled.
This is particularly necessary during
Task:await
. When the programmer runstask:await()
, it may throwerror('cancelled')
, but, without thecancelled
flag, it’s unclear whether it comes fromtask
or the running task.