Symmetric coroutines!

This commit is contained in:
Bob Nystrom
2014-04-14 07:23:05 -07:00
parent cd75e17f0d
commit f03895b17e
31 changed files with 276 additions and 63 deletions

View File

@ -17,7 +17,7 @@ EXPECT_ERROR_PATTERN = re.compile(r'// expect error')
EXPECT_ERROR_LINE_PATTERN = re.compile(r'// expect error line (\d+)')
EXPECT_RUNTIME_ERROR_PATTERN = re.compile(r'// expect runtime error: (.+)')
ERROR_PATTERN = re.compile(r'\[.* line (\d+)\] Error')
STACK_TRACE_PATTERN = re.compile(r'\[.* line (\d+)\] in \(script\)')
STACK_TRACE_PATTERN = re.compile(r'\[.* line (\d+)\] in')
SKIP_PATTERN = re.compile(r'// skip: (.*)')
NONTEST_PATTERN = re.compile(r'// nontest')

View File

@ -224,18 +224,13 @@ DEF_NATIVE(fiber_new)
RETURN_OBJ(newFiber);
}
DEF_NATIVE(fiber_isDone)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
RETURN_BOOL(runFiber->numFrames == 0);
}
DEF_NATIVE(fiber_run)
DEF_NATIVE(fiber_call)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
if (runFiber->numFrames == 0) RETURN_ERROR("Cannot run a finished fiber.");
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;
@ -248,11 +243,12 @@ DEF_NATIVE(fiber_run)
return PRIM_RUN_FIBER;
}
DEF_NATIVE(fiber_run1)
DEF_NATIVE(fiber_call1)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
if (runFiber->numFrames == 0) RETURN_ERROR("Cannot run a finished fiber.");
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;
@ -273,15 +269,69 @@ DEF_NATIVE(fiber_run1)
return PRIM_RUN_FIBER;
}
DEF_NATIVE(fiber_isDone)
{
ObjFiber* runFiber = AS_FIBER(args[0]);
RETURN_BOOL(runFiber->numFrames == 0);
}
DEF_NATIVE(fiber_run)
{
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->stackSize > 0)
{
runFiber->stack[runFiber->stackSize - 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;
return PRIM_RUN_FIBER;
}
DEF_NATIVE(fiber_run1)
{
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 the value passed to
// run.
if (runFiber->caller == NULL && runFiber->stackSize > 0)
{
runFiber->stack[runFiber->stackSize - 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_NATIVE(fiber_yield)
{
if (fiber->caller == NULL) RETURN_ERROR("No fiber to yield to.");
ObjFiber* caller = fiber->caller;
fiber->caller = NULL;
// Make the caller's run method return null.
fiber->caller->stack[fiber->caller->stackSize - 1] = NULL_VAL;
caller->stack[caller->stackSize - 1] = NULL_VAL;
// Return the fiber to resume.
args[0] = OBJ_VAL(fiber->caller);
args[0] = OBJ_VAL(caller);
return PRIM_RUN_FIBER;
}
@ -289,8 +339,11 @@ DEF_NATIVE(fiber_yield1)
{
if (fiber->caller == NULL) RETURN_ERROR("No fiber to yield to.");
ObjFiber* caller = fiber->caller;
fiber->caller = NULL;
// Make the caller's run method return the argument passed to yield.
fiber->caller->stack[fiber->caller->stackSize - 1] = args[1];
caller->stack[caller->stackSize - 1] = args[1];
// When the yielding fiber resumes, we'll store the result of the yield call
// in its stack. Since Fiber.yield(value) has two arguments (the Fiber class
@ -299,7 +352,7 @@ DEF_NATIVE(fiber_yield1)
fiber->stackSize--;
// Return the fiber to resume.
args[0] = OBJ_VAL(fiber->caller);
args[0] = OBJ_VAL(caller);
return PRIM_RUN_FIBER;
}
@ -966,16 +1019,17 @@ void wrenInitializeCore(WrenVM* vm)
NATIVE(vm->boolClass, "toString", bool_toString);
NATIVE(vm->boolClass, "!", bool_not);
// TODO: Make fibers inherit Sequence and be iterable.
vm->fiberClass = defineClass(vm, "Fiber");
NATIVE(vm->fiberClass->metaclass, " instantiate", fiber_instantiate);
NATIVE(vm->fiberClass->metaclass, "new ", fiber_new);
NATIVE(vm->fiberClass->metaclass, "yield", fiber_yield);
NATIVE(vm->fiberClass->metaclass, "yield ", fiber_yield1);
NATIVE(vm->fiberClass, "call", fiber_call);
NATIVE(vm->fiberClass, "call ", fiber_call1);
NATIVE(vm->fiberClass, "isDone", fiber_isDone);
NATIVE(vm->fiberClass, "run", fiber_run);
NATIVE(vm->fiberClass, "run ", fiber_run1);
// TODO: Primitives for switching to a fiber without setting the caller.
// (I.e. symmetric coroutines.)
vm->fnClass = defineClass(vm, "Fn");

7
test/fiber/call.wren Normal file
View File

@ -0,0 +1,7 @@
var fiber = new Fiber {
IO.print("fiber")
}
IO.print("before") // expect: before
fiber.call // expect: fiber
IO.print("after") // expect: after

View File

@ -0,0 +1,7 @@
var fiber
fiber = new Fiber {
fiber.call // expect runtime error: Fiber has already been called.
}
fiber.call

View File

@ -0,0 +1,12 @@
var a
var b
a = new Fiber {
b.call // expect runtime error: Fiber has already been called.
}
b = new Fiber {
a.call
}
b.call

View File

@ -0,0 +1,6 @@
var fiber = new Fiber {
IO.print("fiber")
}
var result = fiber.call // expect: fiber
IO.print(result) // expect: null

View File

@ -0,0 +1,7 @@
var fiber = new Fiber {
IO.print("fiber")
return "result"
}
var result = fiber.call // expect: fiber
IO.print(result) // expect: result

View File

@ -0,0 +1,6 @@
var fiber = new Fiber {
IO.print("call")
}
fiber.call // expect: call
fiber.call // expect runtime error: Cannot call a finished fiber.

View File

@ -0,0 +1,9 @@
var fiber = new Fiber {
IO.print("fiber")
}
// The first value passed to the fiber is ignored, since there's no yield call
// to return it.
IO.print("before") // expect: before
fiber.call("ignored") // expect: fiber
IO.print("after") // expect: after

View File

@ -0,0 +1,7 @@
var fiber
fiber = new Fiber {
fiber.call(2) // expect runtime error: Fiber has already been called.
}
fiber.call(1)

View File

@ -0,0 +1,12 @@
var a
var b
a = new Fiber {
b.call(3) // expect runtime error: Fiber has already been called.
}
b = new Fiber {
a.call(2)
}
b.call(1)

View File

@ -0,0 +1,6 @@
var fiber = new Fiber {
IO.print("call")
}
fiber.call(1) // expect: call
fiber.call(2) // expect runtime error: Cannot call a finished fiber.

View File

@ -17,9 +17,9 @@ var closure
}
}
fiber.run // expect: before
fiber.call // expect: before
closure.call // expect: before
fiber.run
fiber.call
closure.call // expect: after
fiber.run // expect: after
fiber.call // expect: after
closure.call // expect: final

View File

@ -5,7 +5,7 @@ var fiber = new Fiber {
}
IO.print(fiber.isDone) // expect: false
fiber.run // expect: 1
fiber.call // expect: 1
IO.print(fiber.isDone) // expect: false
fiber.run // expect: 2
fiber.call // expect: 2
IO.print(fiber.isDone) // expect: true

View File

@ -4,12 +4,12 @@ var b = new Fiber {
var a = new Fiber {
IO.print("begin fiber a")
b.run
b.call
IO.print("end fiber a")
}
IO.print("begin main")
a.run
a.call
IO.print("end main")
// expect: begin main

View File

@ -4,6 +4,6 @@ var fiber = new Fiber {
IO.print("before") // expect: before
fiber.run // expect: fiber
IO.print("after") // expect: after
// TODO: Test handles error if fiber tries to run itself.
// This does not get run since we exit when the run fiber completes.
IO.print("nope")

View File

@ -0,0 +1,10 @@
var fiber
fiber = new Fiber {
IO.print(1) // expect: 1
fiber.run
IO.print(2) // expect: 2
}
fiber.call
IO.print(3) // expect: 3

View File

@ -0,0 +1,20 @@
var a
var b
a = new Fiber {
IO.print(2)
b.run
IO.print("nope")
}
b = new Fiber {
IO.print(1)
a.run
IO.print(3)
}
b.call
// expect: 1
// expect: 2
// expect: 3
IO.print(4) // expect: 4

View File

@ -1,6 +0,0 @@
var fiber = new Fiber {
IO.print("fiber")
}
var result = fiber.run // expect: fiber
IO.print(result) // expect: null

View File

@ -1,7 +0,0 @@
var fiber = new Fiber {
IO.print("fiber")
return "result"
}
var result = fiber.run // expect: fiber
IO.print(result) // expect: result

View File

@ -1,6 +1,13 @@
var fiber = new Fiber {
var a = new Fiber {
IO.print("run")
}
fiber.run // expect: run
fiber.run // expect runtime error: Cannot run a finished fiber.
// Run a through an intermediate fiber since it will get discarded and we need
// to return to the main one after a completes.
var b = new Fiber {
a.run
IO.print("nope")
}
b.call // expect: run
a.run // expect runtime error: Cannot run a finished fiber.

View File

@ -4,6 +4,8 @@ var fiber = new Fiber {
// The first value passed to the fiber is ignored, since there's no yield call
// to return it.
IO.print("before") // expect: before
IO.print("before") // expect: before
fiber.run("ignored") // expect: fiber
IO.print("after") // expect: after
// This does not get run since we exit when the run fiber completes.
IO.print("nope")

View File

@ -0,0 +1,10 @@
var fiber
fiber = new Fiber {
IO.print(1) // expect: 1
fiber.run("ignored")
IO.print(2) // expect: 2
}
fiber.call
IO.print(3) // expect: 3

View File

@ -0,0 +1,20 @@
var a
var b
a = new Fiber {
IO.print(2)
b.run("ignored")
IO.print("nope")
}
b = new Fiber {
IO.print(1)
a.run("ignored")
IO.print(3)
}
b.call
// expect: 1
// expect: 2
// expect: 3
IO.print(4) // expect: 4

View File

@ -1,6 +0,0 @@
var fiber = new Fiber {
IO.print("run")
}
fiber.run(1) // expect: run
fiber.run(2) // expect runtime error: Cannot run a finished fiber.

View File

@ -6,9 +6,9 @@ var fiber = new Fiber {
IO.print("fiber 3")
}
var result = fiber.run // expect: fiber 1
IO.print("main 1") // expect: main 1
result = fiber.run // expect: fiber 2
IO.print("main 2") // expect: main 2
result = fiber.run // expect: fiber 3
IO.print("main 3") // expect: main 3
var result = fiber.call // expect: fiber 1
IO.print("main 1") // expect: main 1
result = fiber.call // expect: fiber 2
IO.print("main 2") // expect: main 2
result = fiber.call // expect: fiber 3
IO.print("main 3") // expect: main 3

View File

@ -6,9 +6,9 @@ var fiber = new Fiber {
IO.print(result)
}
fiber.run // expect: fiber 1
fiber.call // expect: fiber 1
IO.print("main 1") // expect: main 1
fiber.run("run 1") // expect: run 1
fiber.call("call 1") // expect: call 1
IO.print("main 2") // expect: main 2
fiber.run // expect: null
fiber.call // expect: null
IO.print("main 3") // expect: main 3

View File

@ -0,0 +1,12 @@
var fiber = new Fiber {
IO.print("fiber")
var result = Fiber.yield
IO.print(result)
}
fiber.call // expect: fiber
IO.print("main") // expect: main
fiber.run("run") // expect: run
// This does not get run since we exit when the run fiber completes.
IO.print("nope")

View File

@ -0,0 +1,9 @@
var a = new Fiber {
Fiber.yield // expect runtime error: No fiber to yield to.
}
// Run a chain of fibers. Since none of them are called, they all get discarded
// and there is no remaining caller.
var b = new Fiber { a.run }
var c = new Fiber { b.run }
c.run

View File

@ -6,9 +6,9 @@ var fiber = new Fiber {
IO.print("fiber 3")
}
var result = fiber.run // expect: fiber 1
var result = fiber.call // expect: fiber 1
IO.print(result) // expect: yield 1
result = fiber.run // expect: fiber 2
result = fiber.call // expect: fiber 2
IO.print(result) // expect: yield 2
result = fiber.run // expect: fiber 3
result = fiber.call // expect: fiber 3
IO.print(result) // expect: null

View File

@ -0,0 +1,9 @@
var a = new Fiber {
Fiber.yield(1) // expect runtime error: No fiber to yield to.
}
// Run a chain of fibers. Since none of them are called, they all get discarded
// and there is no remaining caller.
var b = new Fiber { a.run }
var c = new Fiber { b.run }
c.run