diff --git a/README.md b/README.md index 99c82b6ab9..3a292cfb1e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ A complete [implementation](https://github.com/effect-handlers/wasm-spec) is ava * See the [examples](proposals/continuations/examples) for Wasm code for implementing various different features including lightweight threads, actors, and async/await. +## Task-based Stack Switching Proposal + +An alternate approach to stack switching revolves around the concept of tasks rather than continuation functions. An explainer for this proposal can be seen [here](proposals/tasks/Explainer.md). + Original `README` from upstream repository follows. # spec diff --git a/proposals/tasks/Explainer.md b/proposals/tasks/Explainer.md new file mode 100644 index 0000000000..b323b2b3d3 --- /dev/null +++ b/proposals/tasks/Explainer.md @@ -0,0 +1,505 @@ +# Task Oriented Stack Switching + +## Motivation + +Non-local control flow (sometimes called _stack switching_) operators allow applications to manage their own computations. Technically, this means that an application can partition its logic into separable computations and can manage (i.e., schedule) their execution. Pragmatically, this enables a range of control flows that include supporting the handling of asynchronous events, supporting cooperatively scheduled threads of execution, and supporting yield-style iterators and consumers. + +This proposal refers solely to those features of the core WebAssembly virtual machine needed to support non-local control flow and does not attempt to preclude any particular strategy for implementing high level languages. We also aim for a minimal spanning set of concepts: the author of a high level language compiler should find all the necessary elements to enable their compiler to generate appropriate WebAssembly code fragments. + +## Tasks and Events + +The main concepts in this proposal are the _task_ and the _event_. A task is a first class value which denotes a computation. An event is used to signal a change in the flow of computation from one task to another. + +### The `task` abstraction + +A `task` denotes a computation that has an extent in time and may be referenced. The latter allows applications to manage their own computations and support patterns such as asynch/await, green threads and yield-style generators. + +Associated with tasks are a new type: + +``` +taskref +``` +which denotes tasks. + +Notice that our `taskref` type is not qualified: tasks are not expected to return in the normal way that functions do. We explore this topic again in [Frequently Asked Questions](#frequently-asked-questions). + + +#### The state of a task + +A task is inherently stateful; as the computation proceeds the task's state reflects that evolution. The state of a task includes function call frames and local variables that are currently live within the computation as well as whether the task is currently being executed. + +However, apart from when a significant event occurs within the computation, the state of a task is not externally visible. + +In our descriptions of task states it is convenient to identify an enumerated symbol that denotes the execution state of the task: + +``` +typedef enum { + suspended, + active, + moribund +} TaskExecutionState +``` + +Only tasks that are in the `active` state may be suspended, and only tasks that are in the `suspended` state may be resumed. `moribund` tasks are those that are no longer capable of execution but may still be referenced in some way. The execution state of a task is not directly inspectable by WebAssembly instructions. + +#### Tasks, anscestors and children + +It is convenient to identify some relationships between tasks: the parent of a task is the task that most recently resumed that task. + +The root of the ancestor relation is the _root task_ and represents the initial entry point into WebAssembly execution. + +The root task does not have an explicit identity. This is because, in general, child tasks cannot manage their ancestors. + +### The `event` abstraction + +An event is an occurrence where computation switches between tasks. Events also represent a communications opportunity between tasks: an event may communicate data as well as signal a change in tasks. + +#### Event declaration +An event has a predeclared `tag` which determines the type of event and what values are associated with the event. Event tags are declared: + +``` +(tag $e (param t*)) +``` +where the parameter types are the types of values communicated along with the event. Event tags may be exported from modules and imported into modules. + +Every change in computation is associated with an event: whenever a task is suspended, the suspending task uses an event to signal both the reason for the suspension and to communicate any necessary data. Similarly, when a task is resumed, an event is used to signal to the resumed task the reason for the resumption. + +## Instructions + +We introduce instructions for managing tasks and instructions for signalling and responding to events. + +### Task instructions + +#### `task.new` Create a new task + +The `task.new` instruction creates a new task entity. The instruction has a literal operand which is the index of a function of type `[taskref t*]->[]`, together with corresponding values on the argument stack. + +The result is a `taskref` which is the identifier for the newly created task. The identity of the task is also the first argument to the task function itself—this allows tasks to know their own identity in a straightforward way. + +The task itself is created in a `suspended` state: it must be the case that the first executable instruction in the function body is an `event.switch` instruction. + +#### `task.suspend` Suspend an active task + +The `task.suspend` instruction takes a task as an argument and suspends the task. The identified task must be in the `active` state—but it need not be the most recently activated task: it may be an ancestor of the most recent task. The _root_ ancestor task does not have an explicit identifier; and so it may not be suspended. + +All the tasks between the most recently activated task and the identified task inclusive are marked as `suspended`. + +`task.suspend` has two operands: the identity of the task being suspended and a description of the event it is signaling: the `event` tag and any arguments to the event. The event operands must be on the argument stack. + +The instruction following the `task.suspend` must be an `event.switch` instruction. + +#### `task.resume` Resume a suspended task + +The `task.resume` instruction takes a task as argument, together with an `event` description—consisting of an event tag and possible values, and resumes its execution. + +The `task.resume` instruction takes a `suspended` task, together with any descendant tasks that were suspended along with it, and resumes its execution. The event is used to encode how the resumed task should react: for example, whether the task's requested information is available, or whether the task should enter into cancelation mode. + +#### `task.switchto` Switch to a different task + +The `task.switchto` instruction is a combination of a `task.suspend` and a `task.resume` to an identified task. This instruction is useful for circumstances where the suspending task knows which other task should be resumed. + +The `task.switchto` instruction has three arguments: the identity of the task being suspended, the identity of the task being resumed and the signaling event. + +Although it may be viewed as being a combination of the two instructions, there is an important distinction also: the signaling event. Under the common hierarchical organization, a suspending task does not know which task will be resumed. This means that the signaling event has to be of a form that the task's manager is ready to process. However, with a `task.switchto` instruction, the task's manager is not informed of the switch and does not need to understand the signaling event. + +This, in turn, means that a task manager may be relieved of the burden of communicating between tasks. I.e., `task.switchto` supports a symmetric coroutining pattern. However, precisely because the task's manager is not made aware of the switch between tasks, it must also be the case that this does not _matter_; in effect, the task manager may not directly be aware of any of the tasks that it is managing. + +#### `task.retire` Retire a task + +The `task.retire` instruction is used when a task has finished its work and wishes to inform its parent of any final results. Like `task.suspend` (and `task.resume`), `task.retire` has an event argument—together with associated values on the agument stack— that are communicated. + +In addition, the retiring task is put into a `moribund` state and any computation resources associated with it are released. If the task has any active descendants then they too are made `moribund`. + +#### `task.release` Destroy a suspended task + +The `task.release` instruction clears any computation resources associated with the identified task. The identified task must be in `suspended` state. + +If the suspended task has current descendant tasks (such as when the task was suspended), then those tasks are `task.release`d also. + +Since task references are wasm values, the reference itself remains valid. However, the task itself is now in a `moribund` state that cannot be resumed. + +The `task.release` instruction is primarily intended for situations where a task manage needs to eliminate unneeded task and does not wish to formally cancel them. + +### Event Instructions + +The main event instruction is `event.switch`; which is used to react to an event. + +#### `event.switch` + +The `event.switch` instruction takes a list of pairs as a literal operand. Each pair consists of the identity of an event tag and a block label. + +If an event is signaled that is not in the list of event/label pairs then the engine traps: there is no fall back or stack search implied by this instruction. + +If an event is signaled for which there is an event label in the list, then it must also be the case that the top n elements of the argument stack are present and are of the right type. This is validated by a combination of the event declaration and the type signatures of the identified blocks[^2]. If there is a mismatch in type expectations, then the module does not validate. + +[^2]: If there are more types in the block's result type, then those must correspond to elements of the input of that block. + +## Examples + +We look at three examples in order of increasing complexity and sophistication: a yield-style generator, cooperative threading and handling asynchronous I/O. + +### Yield-style generators + +The so-called yield style generator pattern consists of a pair: a generator function that generates elements and a consumer that consumes those elements. When the generator has found the next element it yields it to the consumer, and when the consumer needs the next element it waits for it. Yield-style generators represents the simplest use case for stack switching in general; which is why we lead with it here. + +#### Generating elements of an array +We start with a simple C-style pseudo-code example of a generator that yields for every element of an array: + +``` +void arrayGenerator(task *thisTask,int count,int els){ + for(int ix=0;ix