mirror of
https://github.com/wren-lang/wren.git
synced 2026-01-11 22:28:45 +01:00
Fibers!
Still have some edge cases to implement but the basics are in place and working now.
This commit is contained in:
100
src/wren_core.c
100
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);
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
101
src/wren_vm.c
101
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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
19
test/fiber/resume_caller.wren
Normal file
19
test/fiber/resume_caller.wren
Normal 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
12
test/fiber/run.wren
Normal 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.
|
||||
6
test/fiber/run_return_implicit_null.wren
Normal file
6
test/fiber/run_return_implicit_null.wren
Normal file
@ -0,0 +1,6 @@
|
||||
var fiber = Fiber.create(fn {
|
||||
IO.print("fiber")
|
||||
})
|
||||
|
||||
var result = fiber.run // expect: fiber
|
||||
IO.print(result) // expect: null
|
||||
7
test/fiber/run_return_value.wren
Normal file
7
test/fiber/run_return_value.wren
Normal 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
|
||||
9
test/fiber/run_with_value.wren
Normal file
9
test/fiber/run_with_value.wren
Normal 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
5
test/fiber/type.wren
Normal 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
14
test/fiber/yield.wren
Normal 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
|
||||
14
test/fiber/yield_return_value.wren
Normal file
14
test/fiber/yield_return_value.wren
Normal 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
|
||||
14
test/fiber/yield_with_value.wren
Normal file
14
test/fiber/yield_with_value.wren
Normal 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
|
||||
Reference in New Issue
Block a user