Still have some edge cases to implement but the basics are in
place and working now.
This commit is contained in:
Bob Nystrom
2014-01-12 11:02:07 -08:00
parent 86d93296ab
commit f3a1f7e677
15 changed files with 306 additions and 40 deletions

View File

@ -176,6 +176,97 @@ DEF_NATIVE(bool_toString)
}
}
DEF_NATIVE(fiber_create)
{
if (!IS_FN(args[1]) && !IS_CLOSURE(args[1]))
{
RETURN_ERROR("Argument must be a function.");
}
ObjFiber* newFiber = wrenNewFiber(vm, AS_OBJ(args[1]));
// The compiler expect the first slot of a function to hold the receiver.
// Since a fiber's stack is invoked directly, it doesn't have one, so put it
// in here.
// TODO: Is there a cleaner solution?
// TODO: If we make growable stacks, make sure this grows it.
newFiber->stack[0] = NULL_VAL;
newFiber->stackSize++;
RETURN_VAL(OBJ_VAL(newFiber));
}
DEF_NATIVE(fiber_run)
{
// TODO: Error if the fiber is already complete.
ObjFiber* runFiber = AS_FIBER(args[0]);
// Remember who ran it.
runFiber->caller = fiber;
// If the fiber was yielded, make the yield call return null.
if (runFiber->stackSize > 0)
{
runFiber->stack[runFiber->stackSize - 1] = NULL_VAL;
}
return PRIM_RUN_FIBER;
}
DEF_NATIVE(fiber_run1)
{
// TODO: Error if the fiber is already complete.
ObjFiber* runFiber = AS_FIBER(args[0]);
// Remember who ran it.
runFiber->caller = fiber;
// If the fiber was yielded, make the yield call return the value passed to
// run.
if (runFiber->stackSize > 0)
{
runFiber->stack[runFiber->stackSize - 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->stackSize--;
return PRIM_RUN_FIBER;
}
DEF_NATIVE(fiber_yield)
{
// TODO: Handle caller being null.
// Make the caller's run method return null.
fiber->caller->stack[fiber->caller->stackSize - 1] = NULL_VAL;
// Return the fiber to resume.
args[0] = OBJ_VAL(fiber->caller);
return PRIM_RUN_FIBER;
}
DEF_NATIVE(fiber_yield1)
{
// TODO: Handle caller being null.
// Make the caller's run method return the argument passed to yield.
fiber->caller->stack[fiber->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
// and the value) and we only need one slot for the result, discard the other
// slot now.
fiber->stackSize--;
// Return the fiber to resume.
args[0] = OBJ_VAL(fiber->caller);
return PRIM_RUN_FIBER;
}
DEF_NATIVE(fn_call) { return PRIM_CALL; }
DEF_NATIVE(list_add)
@ -603,6 +694,15 @@ void wrenInitializeCore(WrenVM* vm)
NATIVE(vm->boolClass, "!", bool_not);
vm->fiberClass = defineClass(vm, "Fiber");
// TODO: Is there a way we can make this a regular constructor?
NATIVE(vm->fiberClass->metaclass, "create ", fiber_create);
NATIVE(vm->fiberClass->metaclass, "yield", fiber_yield);
NATIVE(vm->fiberClass->metaclass, "yield ", fiber_yield1);
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.) Also a getter to tell if a fiber is complete
// or not.
vm->fnClass = defineClass(vm, "Function");
NATIVE(vm->fnClass, "call", fn_call);

View File

@ -350,7 +350,7 @@ void wrenDebugPrintCode(WrenVM* vm, ObjFn* fn)
void wrenDebugPrintStack(ObjFiber* fiber)
{
printf(":: ");
printf("(fiber %p) ", fiber);
for (int i = 0; i < fiber->stackSize; i++)
{
wrenPrintValue(fiber->stack[i]);

View File

@ -129,14 +129,28 @@ ObjClosure* wrenNewClosure(WrenVM* vm, ObjFn* fn)
return closure;
}
ObjFiber* wrenNewFiber(WrenVM* vm)
ObjFiber* wrenNewFiber(WrenVM* vm, Obj* fn)
{
ObjFiber* fiber = allocate(vm, sizeof(ObjFiber));
initObj(vm, &fiber->obj, OBJ_FIBER);
// Push the stack frame for the function.
fiber->stackSize = 0;
fiber->numFrames = 0;
fiber->numFrames = 1;
fiber->openUpvalues = NULL;
fiber->caller = NULL;
CallFrame* frame = &fiber->frames[0];
frame->fn = fn;
frame->stackStart = 0;
if (fn->type == OBJ_FN)
{
frame->ip = ((ObjFn*)fn)->bytecode;
}
else
{
frame->ip = ((ObjClosure*)fn)->fn->bytecode;
}
return fiber;
}

View File

@ -138,7 +138,7 @@ typedef struct
int stackStart;
} CallFrame;
typedef struct
typedef struct sObjFiber
{
Obj obj;
Value stack[STACK_SIZE];
@ -151,6 +151,10 @@ typedef struct
// pointing to values still on the stack. The head of the list will be the
// upvalue closest to the top of the stack, and then the list works downwards.
Upvalue* openUpvalues;
// The fiber that ran this one. If this fiber is yielded, control will resume
// to this one. May be `NULL`.
struct sObjFiber* caller;
} ObjFiber;
typedef enum
@ -162,7 +166,11 @@ typedef enum
PRIM_ERROR,
// A new callframe has been pushed.
PRIM_CALL
PRIM_CALL,
// A fiber is being switched to.
PRIM_RUN_FIBER
} PrimitiveResult;
typedef struct
@ -306,6 +314,9 @@ typedef struct
// Value -> ObjClosure*.
#define AS_CLOSURE(value) ((ObjClosure*)AS_OBJ(value))
// Value -> ObjFiber*.
#define AS_FIBER(v) ((ObjFiber*)AS_OBJ(v))
// Value -> ObjFn*.
#define AS_FN(value) ((ObjFn*)AS_OBJ(value))
@ -500,8 +511,9 @@ void wrenBindMethod(WrenVM* vm, ObjClass* classObj, int symbol, Method method);
// upvalues, but assumes outside code will populate it.
ObjClosure* wrenNewClosure(WrenVM* vm, ObjFn* fn);
// Creates a new fiber object.
ObjFiber* wrenNewFiber(WrenVM* vm);
// Creates a new fiber object that will invoke [fn], which can be a function or
// closure.
ObjFiber* wrenNewFiber(WrenVM* vm, Obj* fn);
// TODO: The argument list here is getting a bit gratuitous.
// Creates a new function object with the given code and constants. The new

View File

@ -61,6 +61,7 @@ WrenVM* wrenNewVM(WrenConfiguration* configuration)
}
vm->compiler = NULL;
vm->fiber = NULL;
vm->first = NULL;
vm->pinned = NULL;
@ -83,6 +84,8 @@ void wrenFreeVM(WrenVM* vm)
wrenSymbolTableClear(vm, &vm->methods);
wrenSymbolTableClear(vm, &vm->globalSymbols);
wrenReallocate(vm, vm, 0, 0);
// TODO: Need to free all allocated objects.
}
static void collectGarbage(WrenVM* vm);
@ -222,6 +225,9 @@ static void markFiber(WrenVM* vm, ObjFiber* fiber)
markUpvalue(vm, upvalue);
upvalue = upvalue->next;
}
// The caller.
if (fiber->caller != NULL) markFiber(vm, fiber->caller);
}
static void markClosure(WrenVM* vm, ObjClosure* closure)
@ -335,6 +341,9 @@ static void collectGarbage(WrenVM* vm)
pinned = pinned->previous;
}
// The current fiber.
if (vm->fiber != NULL) wrenMarkObj(vm, (Obj*)vm->fiber);
// Any object the compiler is using (if there is one).
if (vm->compiler != NULL) wrenMarkCompiler(vm, vm->compiler);
@ -540,8 +549,17 @@ static void callFunction(ObjFiber* fiber, Obj* function, int numArgs)
// The main bytecode interpreter loop. This is where the magic happens. It is
// also, as you can imagine, highly performance critical. Returns `true` if the
// fiber completed without error.
static bool interpret(WrenVM* vm, ObjFiber* fiber)
static bool runInterpreter(WrenVM* vm)
{
// Hoist these into local variables. They are accessed frequently in the loop
// but assigned less frequently. Keeping them in locals and updating them when
// a call frame has been pushed or popped gives a large speed boost.
register ObjFiber* fiber = vm->fiber;
register CallFrame* frame;
register uint8_t* ip;
register ObjFn* fn;
register Upvalue** upvalues;
// These macros are designed to only be invoked within this function.
// TODO: Check for stack overflow.
#define PUSH(value) (fiber->stack[fiber->stackSize++] = value)
@ -551,14 +569,6 @@ static bool interpret(WrenVM* vm, ObjFiber* fiber)
#define READ_BYTE() (*ip++)
#define READ_SHORT() (ip += 2, (ip[-2] << 8) | ip[-1])
// Hoist these into local variables. They are accessed frequently in the loop
// but assigned less frequently. Keeping them in locals and updating them when
// a call frame has been pushed or popped gives a large speed boost.
register CallFrame* frame;
register uint8_t* ip;
register ObjFn* fn;
register Upvalue** upvalues;
// Use this before a CallFrame is pushed to store the local variables back
// into the current one.
#define STORE_FRAME() frame->ip = ip
@ -748,6 +758,12 @@ static bool interpret(WrenVM* vm, ObjFiber* fiber)
callFunction(fiber, AS_OBJ(args[0]), numArgs);
LOAD_FRAME();
break;
case PRIM_RUN_FIBER:
STORE_FRAME();
fiber = AS_FIBER(args[0]);
LOAD_FRAME();
break;
}
break;
}
@ -848,6 +864,12 @@ static bool interpret(WrenVM* vm, ObjFiber* fiber)
callFunction(fiber, AS_OBJ(args[0]), numArgs);
LOAD_FRAME();
break;
case PRIM_RUN_FIBER:
STORE_FRAME();
fiber = AS_FIBER(args[0]);
LOAD_FRAME();
break;
}
break;
}
@ -1046,24 +1068,39 @@ static bool interpret(WrenVM* vm, ObjFiber* fiber)
Value result = POP();
fiber->numFrames--;
// If we are returning from the top-level block, we succeeded.
if (fiber->numFrames == 0) return true;
// Close any upvalues still in scope.
Value* firstValue = &fiber->stack[frame->stackStart];
while (fiber->openUpvalues != NULL &&
fiber->openUpvalues->value >= firstValue)
// If the fiber is complete, end it.
if (fiber->numFrames == 0)
{
closeUpvalue(fiber);
// If this is the main fiber, we're done.
if (fiber->caller == NULL) return true;
// TODO: Do we need to close upvalues here?
// We have a calling fiber to resume.
fiber = fiber->caller;
// Store the result in the resuming fiber.
fiber->stack[fiber->stackSize - 1] = result;
}
else
{
// Close any upvalues still in scope.
Value* firstValue = &fiber->stack[frame->stackStart];
while (fiber->openUpvalues != NULL &&
fiber->openUpvalues->value >= firstValue)
{
closeUpvalue(fiber);
}
// Store the result of the block in the first slot, which is where the
// caller expects it.
fiber->stack[frame->stackStart] = result;
// Discard the stack slots for the call frame (leaving one slot for the
// result).
fiber->stackSize = frame->stackStart + 1;
}
// Store the result of the block in the first slot, which is where the
// caller expects it.
fiber->stack[frame->stackStart] = result;
// Discard the stack slots for the call frame (leaving one slot for the
// result).
fiber->stackSize = frame->stackStart + 1;
LOAD_FRAME();
DISPATCH();
}
@ -1168,25 +1205,25 @@ static bool interpret(WrenVM* vm, ObjFiber* fiber)
int wrenInterpret(WrenVM* vm, const char* sourcePath, const char* source)
{
ObjFiber* fiber = wrenNewFiber(vm);
PinnedObj pinned;
pinObj(vm, (Obj*)fiber, &pinned);
// TODO: Move actual error codes to main.c and return something Wren-specific
// from here.
int result = 0;
ObjFn* fn = wrenCompile(vm, sourcePath, source);
if (fn != NULL)
{
callFunction(fiber, (Obj*)fn, 0);
if (!interpret(vm, fiber)) result = 70; // EX_SOFTWARE.
PinnedObj pinned;
pinObj(vm, (Obj*)fn, &pinned);
vm->fiber = wrenNewFiber(vm, (Obj*)fn);
unpinObj(vm);
if (!runInterpreter(vm)) result = 70; // EX_SOFTWARE.
}
else
{
result = 65; // EX_DATAERR.
}
unpinObj(vm);
return result;
}

View File

@ -212,7 +212,10 @@ struct WrenVM
// allocated objects used by the compiler can be found if a GC is kicked off
// in the middle of a compile.
Compiler* compiler;
// The fiber that is currently running.
ObjFiber* fiber;
// Memory management data:
// The number of bytes that are known to be currently allocated. Includes all

View File

@ -0,0 +1,19 @@
var b = Fiber.create(fn {
IO.print("fiber b")
})
var a = Fiber.create(fn {
IO.print("begin fiber a")
b.run
IO.print("end fiber a")
})
IO.print("begin main")
a.run
IO.print("end main")
// expect: begin main
// expect: begin fiber a
// expect: fiber b
// expect: end fiber a
// expect: end main

12
test/fiber/run.wren Normal file
View File

@ -0,0 +1,12 @@
var fiber = Fiber.create(fn {
IO.print("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.
// TODO: Test create is passed right argument type.
// TODO: Test closing over stuff in fiber function.
// TODO: Test running a finished fiber.

View File

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

View File

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

View File

@ -0,0 +1,9 @@
var fiber = Fiber.create(fn {
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.run("ignored") // expect: fiber
IO.print("after") // expect: after

5
test/fiber/type.wren Normal file
View File

@ -0,0 +1,5 @@
var fiber = Fiber.create(fn null)
IO.print(fiber is Fiber) // expect: true
IO.print(fiber is Object) // expect: true
IO.print(fiber is Bool) // expect: false
IO.print(fiber.type == Fiber) // expect: true

14
test/fiber/yield.wren Normal file
View File

@ -0,0 +1,14 @@
var fiber = Fiber.create(fn {
IO.print("fiber 1")
Fiber.yield
IO.print("fiber 2")
Fiber.yield
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

View File

@ -0,0 +1,14 @@
var fiber = Fiber.create(fn {
IO.print("fiber 1")
var result = Fiber.yield
IO.print(result)
result = Fiber.yield
IO.print(result)
})
fiber.run // expect: fiber 1
IO.print("main 1") // expect: main 1
fiber.run("run 1") // expect: run 1
IO.print("main 2") // expect: main 2
fiber.run // expect: null
IO.print("main 3") // expect: main 3

View File

@ -0,0 +1,14 @@
var fiber = Fiber.create(fn {
IO.print("fiber 1")
Fiber.yield("yield 1")
IO.print("fiber 2")
Fiber.yield("yield 2")
IO.print("fiber 3")
})
var result = fiber.run // expect: fiber 1
IO.print(result) // expect: yield 1
result = fiber.run // expect: fiber 2
IO.print(result) // expect: yield 2
result = fiber.run // expect: fiber 3
IO.print(result) // expect: null