Coroutines
Opened this issue · 8 comments
var i = 0;
def producer = () {
while (true) {
yield consumer;
global::i = global::i + 1;
}
}
def consumer = () {
while (true) {
println("consuming " + global::i);
yield producer;
}
}
producer();
This would be an amazing feature to have, but can it be implemented easily with the current structure? To start, only functions/delegates could have a yield
statement. Storing function variables is easy too. The main problem is pausing execution of a function to yield. How does one do that? It would be relatively simple if no blocks (loops, if, with, etc) existed in the function code.
Perhaps force all EveStatements to implement a yield method that stores state? Then we can immediately return to it. The problem still is: what about blocks and nested blocks? I'm thinking as if Java has continuations, which it doesn't. Perhaps make use of a library...
Upgrade to Enhancement after messing around with javaflow library somewhat and understanding how this will be possible.
To implement coroutines:
- Have a global State object that keeps track of the EveObject that was yielded to.
- Have a Map that keeps track of EveObject/Function -> Continuation (store where we are).
- Loop while that object is not null, executing and swapping coroutines.
- All Function calls must be wrapped in Runnables.
//EveObject?
public void invokeAsCoroutine() {
Runnable r = new Runnable() {
public void run() { EveObject.this.invoke(); }
};
State.yieldTo = this;
while (State.yieldTo != null) {
Continuation cont = getContinuation(State.yieldTo); //if not in map, create new Continuation.
EveObject thisFunction = State.yieldTo;
State.yieldTo = null;
Continuation c = Continuation.continueWith(cont, state); //may have set State.yieldTo.
coroutines.put(thisFunction, c); //keep track of where we were for later.
}
}
//YieldStatement class
//would of course need more support for yielding variables around.
private ExpressionStatement expr;
public EveObject execute() {
State.yieldTo = expr.execute();
Continuation.suspend();
return null;
}
Better Eve syntax?
import("coroutine");
def producer = () {
while (true) {
var i = produceSomeJunk();
yield i to consumer;
}
}
def consumer = () {
while (true) {
var i = coroutine::context["i"];
println("consuming " ~ i);
yield producer;
}
}
producer();
So now that the Javaflow bug is sorted out, the following problems exist:
- When yielding, scope stack does not switch properly.
- Cannot use yield as a return statement for arbitrary variables. It always looks for a function and attempts to run it as a coroutine.
- No error checking with regards to the yield statement. Only functions should be runnable as coroutines.
To fix these:
- New function scope should be pushed when running via yield, and popped afterward.
- Change
yield
to yield any value, and makeyield to <func>
andyield <value> to <func>
be the way to yield to other coroutines. - Just add some type checking in ScopeManager.
Another construct I would like is the ability to write "background" code with the yield statement:
//...
var x = yield to func;
//...
var y = yield x;
//...
A regular yield
statement will return control to the parent scope. A yield to
statement will jump to another function and execute it (or resume it, if it's yielded). The main problem I foresee is this:
def x = () {
var result = yield to y;
}
def y = () {
var result = compute();
yield result;
}
Above, we would expect the "parent" of y
to be x
. If y was invoked normally, its parent would be the global namespace environment. But, how do we really determine what the "parent" of Y is? If, somewhere along the way, another function called by y
yields to y
, after x
has already yielded to y
, how do we handle that?
The easiest way is to disallow functions from yielding to functions that have already been yielded to. But is that inflexible?
In any event, functions will need to know what their "parent" is. I think that can be handled by passing the "parent" in as the state object for the continuation. This will always be an EveObject
representing the scope to return to. But then we get into questions of how to handle that with the stack?
ANTLR problems:
Forgot that to
is the range operator. Need a better syntax for coroutines. Could use exec
perhaps? Could just expose the things as fibers and use that as an operator...
var x = fiber func;
var x = exec func;
var x = run func;
Also having a problem with yielding of variables to functions. Should probably take the easy way out and expose a send
method or something.
resume .. with
can duplicate send
functionality. Here's a possible final solution:
yield
can only be used to yield a value. It never jumps to a specific place. It will always go to the "parent".resume
is for jumping to specific coroutines, possibly with a value viawith
attached on the end.yield
should also be able to simply stop execution without returning a value, i.e.yield;
def x = () {
var result = resume y;
println(result);
}
def y = () {
resume x with 5;
}
x(); //should print 5.
An equivalent example using yield:
def x = () {
var result = resume y;
println(result);
}
def y = () {
yield 5;
}
x(); //should print 5.
Invoke all functions as continuations, just in case!
Cut losses. Shelved for now.