diff --git a/src/wren_core.c b/src/wren_core.c index 8210b1fc..c7515ed8 100644 --- a/src/wren_core.c +++ b/src/wren_core.c @@ -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); diff --git a/src/wren_debug.c b/src/wren_debug.c index 1421677e..1f2a23a1 100644 --- a/src/wren_debug.c +++ b/src/wren_debug.c @@ -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]); diff --git a/src/wren_value.c b/src/wren_value.c index cd5252ec..631abeb3 100644 --- a/src/wren_value.c +++ b/src/wren_value.c @@ -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; } diff --git a/src/wren_value.h b/src/wren_value.h index b97759e2..d1141845 100644 --- a/src/wren_value.h +++ b/src/wren_value.h @@ -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 diff --git a/src/wren_vm.c b/src/wren_vm.c index 07da5372..8e23b750 100644 --- a/src/wren_vm.c +++ b/src/wren_vm.c @@ -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; } diff --git a/src/wren_vm.h b/src/wren_vm.h index 1360c93e..b5c16a44 100644 --- a/src/wren_vm.h +++ b/src/wren_vm.h @@ -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 diff --git a/test/fiber/resume_caller.wren b/test/fiber/resume_caller.wren new file mode 100644 index 00000000..502e9bca --- /dev/null +++ b/test/fiber/resume_caller.wren @@ -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 diff --git a/test/fiber/run.wren b/test/fiber/run.wren new file mode 100644 index 00000000..fd54fd9f --- /dev/null +++ b/test/fiber/run.wren @@ -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. diff --git a/test/fiber/run_return_implicit_null.wren b/test/fiber/run_return_implicit_null.wren new file mode 100644 index 00000000..04616807 --- /dev/null +++ b/test/fiber/run_return_implicit_null.wren @@ -0,0 +1,6 @@ +var fiber = Fiber.create(fn { + IO.print("fiber") +}) + +var result = fiber.run // expect: fiber +IO.print(result) // expect: null diff --git a/test/fiber/run_return_value.wren b/test/fiber/run_return_value.wren new file mode 100644 index 00000000..5414019e --- /dev/null +++ b/test/fiber/run_return_value.wren @@ -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 diff --git a/test/fiber/run_with_value.wren b/test/fiber/run_with_value.wren new file mode 100644 index 00000000..d41315be --- /dev/null +++ b/test/fiber/run_with_value.wren @@ -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 diff --git a/test/fiber/type.wren b/test/fiber/type.wren new file mode 100644 index 00000000..14ecb594 --- /dev/null +++ b/test/fiber/type.wren @@ -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 diff --git a/test/fiber/yield.wren b/test/fiber/yield.wren new file mode 100644 index 00000000..ef8092db --- /dev/null +++ b/test/fiber/yield.wren @@ -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 diff --git a/test/fiber/yield_return_value.wren b/test/fiber/yield_return_value.wren new file mode 100644 index 00000000..cb03725a --- /dev/null +++ b/test/fiber/yield_return_value.wren @@ -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 diff --git a/test/fiber/yield_with_value.wren b/test/fiber/yield_with_value.wren new file mode 100644 index 00000000..14198476 --- /dev/null +++ b/test/fiber/yield_with_value.wren @@ -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