View on GitHub

TaggedCoro 1.0.0

Tagged (nested) coroutines for Lua, implemented as a C module

Download this project as a .zip file Download this project as a tar.gz file

Build Status Coverage Status

Overview

This module is is a replacement to the standard coroutine module that adds tagged coroutines. Functions create and wrap now receive a tag and a function, instead of just a function. Function yield now also needs a tag as the first argument. The tag can be any Lua value.

A yield with a specific tag yields to the dynamically closest resume of a coroutine with that tag (making an analogy with exceptions: the coroutine is like a try/catch block, yield is like a throw, and the tag is analogous with the type of the exception). If there is no coroutine with the tag an error is thrown at the point of the yield.

On a successful yield, any coroutine that has been passed through in the search for the coroutine that handled that yield becomes stacked, a new status string returned by the status function. Attempting to directly resume a stacked coroutine is an error. Resuming the coroutine that handled the yield rewinds the whole stack, resuming the stacked coroutines along the way until reaching and finally continuing from the point of the original yield.

A failed yield can be an expensive operation, so if you are unsure if you can yield you can use the extended isyieldable function, which now expects a tag and will return true only if yielding with this tag will succeed.

The function coroutine.yield is an untagged yield. A tagged coroutine passes an untagged yield along, unless its parent is the main coroutine: in this case, the yield is supressed, and the source resumes from the untagged yield with the call to yield returning nil and "untagged coroutine not found". Unfortunately there is no way to make the untagged yield fail as if it had tried to yield outside a coroutine.

When an untagged yield reaches an untagged parent, the parent will suspend as if the yield was intended for it; when the parent resumes the whole stack will be resumed, ultimately resuming from the point of the untagged yield. This way you can have a stack of tagged coroutines on top of an untagged coroutine (allowing the use of existing coroutine schedulers, for example).

A tagged yield that reaches an untagged coroutine fails at the point of the call to yield as if it had reached the main coroutine.

A new function call resumes a coroutine as if it had been wrapped by wrap: any uncaught errors while running the coroutine will be propagated. But the stack is not unwound: you can still get a traceback of the full stack of the dead coroutine (including all of the coroutines that were stacked above it) using the new traceback function. It is similar to debug.traceback, except that it includes a full traceback, following source to reach the source of the error and tracing parent back to the main thread.

A new tag function returns the tag of a coroutine. A parent function returns the coroutine that last resumed a coroutine. A source function returns, for a given coroutine, either the coroutine where the last yield came from, in case of a suspended coroutine, or where an error originated, in case of a dead coroutine. You can use these two functions to walk a dead stack of coroutines with the debug functions in case traceback is not enough.

Finally, the function fortag receives a tag and returns a set of tagged coroutine functions specialized for that tag. For compatibility with lua-coronest there is also a make function that is like fortag except it generates a fresh tag if none is given.

There is both a C and a pure Lua implementation. The C implementation is more efficient, and produces better stacktraces, but requires stock Lua 5.2 or higher (it will not work with LuaJIT 2). The pure Lua implementation should work on LuaJIT 2, Lua 5.2, or Lua 5.3, but the isyieldable might give a false positive if there are pending unyieldable C calls in the stack on any Lua version except Lua 5.3.

Installation

Get the latest release of the C module from LuaRocks:

luarocks install taggedcoro

Or if you want the pure Lua version:

luarocks install taggedcoro-purelua

If you want to install from HEAD, download a tarball/zip of this repository or clone it, then run luarocks make on one of the provided rockspecs.

The C module is compatible with both Lua 5.2 and Lua 5.3. The Lua module is compatible with LuaJIT 2, Lua 5.2, and Lua 5.3.

Module reference

All the functions below are exported by the module.

create(tag, f)

Creates a new tagged coroutine, with tag tag (any Lua value except nil) and body f (must be a function). Returns this new coroutine. Like the standard coroutine library, a coroutine is an object with type "thread".

isyieldable(tag)

Returns true when a yield with tag tag will not fail, otherwise it will return false.

resume(co, ...)

Starts or continues the execution of the tagged coroutine co. It works just like coroutine.resume.

call(co, ...)

Starts or continues the execution of the tagged coroutine co, but any errors are propagated, and the initial boolean is not returned in case of success (either because the coroutine yielded or it finished running). The source of the error is still recorded (see source below) so the stack can be inspected later.

running()

Returns the running coroutine plus a boolean, true when the running coroutine is the main one.

status(co)

Returns the status of the (tagged or not) coroutine co, as a string. Returns the same status strings as coroutine.status, plus a new one for tagged coroutines: "stacked". A coroutine is "stacked" if it has been suspended by a yield but is not the coroutine that handled that yield (because of a different tag). A "stacked" coroutine cannot be resumed directly.

wrap(co) or wrap(tag, f)

If called with a tagged coroutine, returns a function that calls call on this coroutine plus any arguments to the function. If called with a tag tag and a function f, creates a new coroutine and then returns both a function that calls call on this coroutine and the coroutine itself.

yield(tag, ...)

Suspends the execution of the nearest coroutine with the tag tag (tag equality is checked with ==, not rawequal). Any arguments to yield are returned by the resume or call call that last resumed this coroutine. Any interving coroutines are stacked (see status, above).

If there is no coroutine with a matching tag yield throws an error at the point of its call. It will also throw an error at the point of the call if yielding failed because of non-yieldable C functions.

tag(co)

Returns the tag associated with the tagged coroutine co, or nil if co is an untagged coroutine.

parent(co)

Returns the coroutine that last resumed the tagged coroutine co (through either resume or call). Returns nil if co is an untagged coroutine, or if the coroutine has been created but not started yet.

source(co)

If the tagged coroutine co is suspended, returns the tagged coroutine that called yield. If co is dead due to an error, returns the tagged coroutine where the error originated. If co is running but caught an error in another coroutine (through resume, pcall, or xpcall), source also returns the tagged coroutine where the error originated. Returns nil in all other cases (including if co is an untagged coroutine).

You can use source and parent to walk the stack (using the debug functions) after an error occurs. The traceback function below uses this to construct a full traceback string analogous to the traceback returned by debug.traceback but for the full coroutine stack, starting from the coroutine that originated the error.

traceback([co,] [message [, level]])

If the first argument is a coroutine, uses the source (see above) of this coroutine as the start of the traceback, otherwise uses the source of the running coroutine as the start. The traceback ends after tracing the main coroutine (or after tracing an untagged coroutine; in this case there will be a message indicating this in the traceback). If a message is present it is appended to the beginning of the traceback. An optional level number tells at which level to start the traceback, on the starting coroutine (default is 1, the top of the call stack). Returns a string with a traceback of the call stack of all coroutines from the starting one, using parent to trace back from it.

fortag(tag)

Returns a table containing all of the functions above (except fortag itself), with the four functions that expect a tag (create, wrap, yield, and isyieldable) specialized for the tag tag (the signatures of these functions do not have a tag parameter).

make([tag])

Like fortag, but generates a fresh tag (an new empty table) if none is given.

install() - Lua 5.2 and Lua 5.3 only

Installs a metatable for all coroutines (tagged and untagged, because all coroutines must share a single metatable). After installing this metatable every coroutine has resume, call, status, tag, parent, and source as methods. Every coroutine can also be called as if it were a function, with the same effect as its call method. Returns the module (so you can both require the module and install the metatable in one step: local tc = require("taggedcoro").install()).

Extras

The contrib folder has sample libraries that implement some abstractions on top of coroutines that can be freely composed with tagged coroutines. The samples folder has sample scripts that exercise these higher-level libraries. Some of them depend on the thread library and on a branch of Cosmo that requires tagged coroutines.

Contact

Please open an issue on github if you would like to report any bugs, or suggest improvements.