Revise low level fiber semantics to play nicer with schedulers.

Now that I'm starting to write a real async scheduler on top of Wren's
basic fiber API, I have a better feel for what it needs. It turns out
run() is not it.

- Remove run() methods.
- Add transfer() which leaves the caller of the invoked fiber alone.
- Add suspend() to return control to the host application.
- Add Timer.schedule() to start a new independently scheduled fiber.
- Change Timer.sleep() so that it only transfers control to explicitly
  scheduled fibers, not any one.
This commit is contained in:
Bob Nystrom
2015-08-30 22:15:37 -07:00
parent 91af02ac81
commit 556af50f83
49 changed files with 469 additions and 350 deletions
+69 -77
View File
@@ -286,49 +286,67 @@ DEF_PRIMITIVE(fiber_abort)
return PRIM_ERROR;
}
// Transfer execution to [fiber] coming from the current fiber whose stack has
// [args].
//
// [isCall] is true if [fiber] is being called and not transferred.
//
// [hasValue] is true if a value in [args] is being passed to the new fiber.
// Otherwise, `null` is implicitly being passed.
static PrimitiveResult runFiber(WrenVM* vm, ObjFiber* fiber, Value* args,
bool isCall, bool hasValue)
{
if (isCall)
{
if (fiber->caller != NULL) RETURN_ERROR("Fiber has already been called.");
// Remember who ran it.
fiber->caller = vm->fiber;
}
else
{
// Edge case: If we are transferring to ourself, immediately return the
// value. We can't treat this like an actual transfer since when we set the
// return below, it would overwrite the fiber being transferred to.
if (fiber == vm->fiber) RETURN_VAL(hasValue ? args[1] : NULL_VAL);
}
if (fiber->numFrames == 0)
{
args[0] = wrenStringFormat(vm, "Cannot $ a finished fiber.",
isCall ? "call" : "transfer to");
return PRIM_ERROR;
}
if (fiber->error != NULL)
{
args[0] = wrenStringFormat(vm, "Cannot $ an aborted fiber.",
isCall ? "call" : "transfer to");
return PRIM_ERROR;
}
// When the calling fiber resumes, we'll store the result of the call in its
// stack. If the call has two arguments (the fiber and the value), we only
// need one slot for the result, so discard the other slot now.
if (hasValue) vm->fiber->stackTop--;
// If the fiber was paused, make yield() or transfer() return the result.
if (fiber->stackTop > fiber->stack)
{
*(fiber->stackTop - 1) = hasValue ? args[1] : NULL_VAL;
}
return PRIM_RUN_FIBER;
}
DEF_PRIMITIVE(fiber_call)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
if (runFiber->numFrames == 0) RETURN_ERROR("Cannot call a finished fiber.");
if (runFiber->caller != NULL) RETURN_ERROR("Fiber has already been called.");
// Remember who ran it.
runFiber->caller = fiber;
// If the fiber was yielded, make the yield call return null.
if (runFiber->stackTop > runFiber->stack)
{
*(runFiber->stackTop - 1) = NULL_VAL;
}
return PRIM_RUN_FIBER;
return runFiber(vm, AS_FIBER(args[0]), args, true, false);
}
DEF_PRIMITIVE(fiber_call1)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
if (runFiber->numFrames == 0) RETURN_ERROR("Cannot call a finished fiber.");
if (runFiber->caller != NULL) RETURN_ERROR("Fiber has already been called.");
// Remember who ran it.
runFiber->caller = fiber;
// If the fiber was yielded, make the yield call return the value passed to
// run.
if (runFiber->stackTop > runFiber->stack)
{
*(runFiber->stackTop - 1) = args[1];
}
// When the calling fiber resumes, we'll store the result of the run call
// in its stack. Since fiber.run(value) has two arguments (the fiber and the
// value) and we only need one slot for the result, discard the other slot
// now.
fiber->stackTop--;
return PRIM_RUN_FIBER;
return runFiber(vm, AS_FIBER(args[0]), args, true, true);
}
DEF_PRIMITIVE(fiber_current)
@@ -349,55 +367,28 @@ DEF_PRIMITIVE(fiber_isDone)
RETURN_BOOL(runFiber->numFrames == 0 || runFiber->error != NULL);
}
DEF_PRIMITIVE(fiber_run)
DEF_PRIMITIVE(fiber_suspend)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
if (runFiber->numFrames == 0) RETURN_ERROR("Cannot run a finished fiber.");
// If the fiber was yielded, make the yield call return null.
if (runFiber->caller == NULL && runFiber->stackTop > runFiber->stack)
{
*(runFiber->stackTop - 1) = NULL_VAL;
}
// Unlike run, this does not remember the calling fiber. Instead, it
// remember's *that* fiber's caller. You can think of it like tail call
// elimination. The switched-from fiber is discarded and when the switched
// to fiber completes or yields, control passes to the switched-from fiber's
// caller.
runFiber->caller = fiber->caller;
// Switching to a null fiber tells the interpreter to stop and exit.
args[0] = NULL_VAL;
return PRIM_RUN_FIBER;
}
DEF_PRIMITIVE(fiber_run1)
DEF_PRIMITIVE(fiber_transfer)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
return runFiber(vm, AS_FIBER(args[0]), args, false, false);
}
if (runFiber->numFrames == 0) RETURN_ERROR("Cannot run a finished fiber.");
// If the fiber was yielded, make the yield call return the value passed to
// run.
if (runFiber->caller == NULL && runFiber->stackTop > runFiber->stack)
{
*(runFiber->stackTop - 1) = args[1];
}
// Unlike run, this does not remember the calling fiber. Instead, it
// remember's *that* fiber's caller. You can think of it like tail call
// elimination. The switched-from fiber is discarded and when the switched
// to fiber completes or yields, control passes to the switched-from fiber's
// caller.
runFiber->caller = fiber->caller;
return PRIM_RUN_FIBER;
DEF_PRIMITIVE(fiber_transfer1)
{
return runFiber(vm, AS_FIBER(args[0]), args, false, true);
}
DEF_PRIMITIVE(fiber_try)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
// TODO: Use runFiber().
if (runFiber->numFrames == 0) RETURN_ERROR("Cannot try a finished fiber.");
if (runFiber->caller != NULL) RETURN_ERROR("Fiber has already been called.");
@@ -420,7 +411,7 @@ DEF_PRIMITIVE(fiber_yield)
ObjFiber* caller = fiber->caller;
fiber->caller = NULL;
fiber->callerIsTrying = false;
// If we don't have any other pending fibers, jump all the way out of the
// interpreter.
if (caller == NULL)
@@ -1350,14 +1341,15 @@ void wrenInitializeCore(WrenVM* vm)
PRIMITIVE(vm->fiberClass->obj.classObj, "new(_)", fiber_new);
PRIMITIVE(vm->fiberClass->obj.classObj, "abort(_)", fiber_abort);
PRIMITIVE(vm->fiberClass->obj.classObj, "current", fiber_current);
PRIMITIVE(vm->fiberClass->obj.classObj, "suspend()", fiber_suspend);
PRIMITIVE(vm->fiberClass->obj.classObj, "yield()", fiber_yield);
PRIMITIVE(vm->fiberClass->obj.classObj, "yield(_)", fiber_yield1);
PRIMITIVE(vm->fiberClass, "call()", fiber_call);
PRIMITIVE(vm->fiberClass, "call(_)", fiber_call1);
PRIMITIVE(vm->fiberClass, "error", fiber_error);
PRIMITIVE(vm->fiberClass, "isDone", fiber_isDone);
PRIMITIVE(vm->fiberClass, "run()", fiber_run);
PRIMITIVE(vm->fiberClass, "run(_)", fiber_run1);
PRIMITIVE(vm->fiberClass, "transfer()", fiber_transfer);
PRIMITIVE(vm->fiberClass, "transfer(_)", fiber_transfer1);
PRIMITIVE(vm->fiberClass, "try()", fiber_try);
vm->fnClass = AS_CLASS(wrenFindVariable(vm, coreModule, "Fn"));
+12 -5
View File
@@ -396,20 +396,24 @@ static void callForeign(WrenVM* vm, ObjFiber* fiber,
//
// Returns the fiber that should receive the error or `NULL` if no fiber
// caught it.
static ObjFiber* runtimeError(ObjFiber* fiber, Value error)
static ObjFiber* runtimeError(WrenVM* vm, ObjFiber* fiber, Value error)
{
ASSERT(fiber->error == NULL, "Can only fail once.");
// Store the error in the fiber so it can be accessed later.
fiber->error = AS_STRING(error);
// Unhook the caller since we will never resume and return to it.
ObjFiber* caller = fiber->caller;
fiber->caller = NULL;
// If the caller ran this fiber using "try", give it the error.
if (fiber->callerIsTrying)
{
ObjFiber* caller = fiber->caller;
// Make the caller's try method return the error message.
*(caller->stackTop - 1) = OBJ_VAL(fiber->error);
vm->fiber = caller;
return caller;
}
@@ -735,7 +739,7 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber)
do \
{ \
STORE_FRAME(); \
fiber = runtimeError(fiber, error); \
fiber = runtimeError(vm, fiber, error); \
if (fiber == NULL) return WREN_RESULT_RUNTIME_ERROR; \
LOAD_FRAME(); \
DISPATCH(); \
@@ -1108,7 +1112,10 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber)
if (fiber->caller == NULL) return WREN_RESULT_SUCCESS;
// We have a calling fiber to resume.
fiber = fiber->caller;
ObjFiber* callingFiber = fiber->caller;
fiber->caller = NULL;
fiber = callingFiber;
vm->fiber = fiber;
// Store the result in the resuming fiber.