From f6cbb6ad755eed76702a9ac97b4523b0214fa0d7 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Wed, 15 Oct 2014 06:36:42 -0700 Subject: [PATCH] Add Fiber.try(). --- doc/site/classes.markdown | 26 +++++++++++++-- src/wren_core.c | 33 ++++++++++++++++++- src/wren_debug.c | 2 +- src/wren_value.c | 11 +++---- src/wren_value.h | 23 +++++++------ src/wren_vm.c | 48 ++++++++++++++++++---------- test/fiber/error.wren | 7 ++++ test/fiber/is_done_after_error.wren | 6 ++++ test/fiber/try.wren | 10 ++++++ test/fiber/try_direct_reenter.wren | 7 ++++ test/fiber/try_indirect_reenter.wren | 12 +++++++ test/fiber/try_when_done.wren | 6 ++++ test/fiber/try_without_error.wren | 8 +++++ 13 files changed, 162 insertions(+), 37 deletions(-) create mode 100644 test/fiber/error.wren create mode 100644 test/fiber/is_done_after_error.wren create mode 100644 test/fiber/try.wren create mode 100644 test/fiber/try_direct_reenter.wren create mode 100644 test/fiber/try_indirect_reenter.wren create mode 100644 test/fiber/try_when_done.wren create mode 100644 test/fiber/try_without_error.wren diff --git a/doc/site/classes.markdown b/doc/site/classes.markdown index fe7f06a5..c28bceb6 100644 --- a/doc/site/classes.markdown +++ b/doc/site/classes.markdown @@ -17,7 +17,7 @@ This creates a class named `Unicorn` with no methods or fields. ## Methods -Once we've made a unicorn, to let it do stuff, we need to give it methods. +To let our unicorn do stuff, we need to give it methods. :::dart class Unicorn { @@ -35,7 +35,7 @@ This defines a `prance` method that takes no arguments. To support parameters, a } } -Unlike most other dynamically-typed languages, in Wren you can have multiple methods in a class with the same name, as long as they take a different number of parameters. In technical terms, you can overload by *arity*. So this class is fine: +Unlike most other dynamically-typed languages, in Wren you can have multiple methods in a class with the same name, as long as they take a different number of parameters. In technical terms, you can *overload by arity*. So this class is fine: :::dart class Unicorn { @@ -118,7 +118,6 @@ Operator overloading is really useful for types like vectors and complex numbers ## Setters -[Assignment](variables.html) *cannot* be overloaded. It isn't an operator, and its semantics are built right into the language. **TODO: ...** @@ -143,8 +142,29 @@ This declares a new class `Pegasus` that inherits from `Unicorn`. The metaclass hierarchy does *not* parallel the regular class hierarchy. So, if `Pegasus` inherits from `Unicorn`, `Pegasus`'s metaclass will not inherit from `Unicorn`'s metaclass. In more prosaic terms, this means that static methods are not inherited. + :::dart + class Unicorn { + // Unicorns cannot fly. :( + static canFly { false } + } + + class Pegasus is Unicorn {} + + Pegasus.canFly // ERROR: Static methods are not inherited. + Constructors, however, initialize the instance *after* it has been created. They are defined as instance methods on the class and not on the metaclass. That means that constructors *are* inherited. + :::dart + class Unicorn { + new(name) { + IO.print("My name is " + name + ".") + } + } + + class Pegasus is Unicorn {} + + new Pegasus("Fred") // Prints "My name is Fred.". + ## Superclass method calls **TODO** diff --git a/src/wren_core.c b/src/wren_core.c index a1215ecb..dec5e1f7 100644 --- a/src/wren_core.c +++ b/src/wren_core.c @@ -269,10 +269,17 @@ DEF_NATIVE(fiber_call1) return PRIM_RUN_FIBER; } +DEF_NATIVE(fiber_error) +{ + ObjFiber* runFiber = AS_FIBER(args[0]); + if (runFiber->error == NULL) RETURN_NULL; + RETURN_OBJ(runFiber->error); +} + DEF_NATIVE(fiber_isDone) { ObjFiber* runFiber = AS_FIBER(args[0]); - RETURN_BOOL(runFiber->numFrames == 0); + RETURN_BOOL(runFiber->numFrames == 0 || runFiber->error != NULL); } DEF_NATIVE(fiber_run) @@ -320,12 +327,33 @@ DEF_NATIVE(fiber_run1) return PRIM_RUN_FIBER; } +DEF_NATIVE(fiber_try) +{ + ObjFiber* runFiber = AS_FIBER(args[0]); + + if (runFiber->numFrames == 0) RETURN_ERROR("Cannot try a finished fiber."); + if (runFiber->caller != NULL) RETURN_ERROR("Fiber has already been called."); + + // Remember who ran it. + runFiber->caller = fiber; + runFiber->callerIsTrying = true; + + // 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_yield) { if (fiber->caller == NULL) RETURN_ERROR("No fiber to yield to."); ObjFiber* caller = fiber->caller; fiber->caller = NULL; + fiber->callerIsTrying = false; // Make the caller's run method return null. caller->stack[caller->stackSize - 1] = NULL_VAL; @@ -341,6 +369,7 @@ DEF_NATIVE(fiber_yield1) ObjFiber* caller = fiber->caller; fiber->caller = NULL; + fiber->callerIsTrying = false; // Make the caller's run method return the argument passed to yield. caller->stack[caller->stackSize - 1] = args[1]; @@ -1027,9 +1056,11 @@ void wrenInitializeCore(WrenVM* vm) NATIVE(vm->fiberClass->metaclass, "yield ", fiber_yield1); NATIVE(vm->fiberClass, "call", fiber_call); NATIVE(vm->fiberClass, "call ", fiber_call1); + NATIVE(vm->fiberClass, "error", fiber_error); NATIVE(vm->fiberClass, "isDone", fiber_isDone); NATIVE(vm->fiberClass, "run", fiber_run); NATIVE(vm->fiberClass, "run ", fiber_run1); + NATIVE(vm->fiberClass, "try", fiber_try); vm->fnClass = defineClass(vm, "Fn"); diff --git a/src/wren_debug.c b/src/wren_debug.c index df3515e4..0d25a1d7 100644 --- a/src/wren_debug.c +++ b/src/wren_debug.c @@ -4,7 +4,7 @@ void wrenDebugPrintStackTrace(WrenVM* vm, ObjFiber* fiber) { - fprintf(stderr, "%s\n", fiber->error); + fprintf(stderr, "%s\n", fiber->error->value); for (int i = fiber->numFrames - 1; i >= 0; i--) { diff --git a/src/wren_value.c b/src/wren_value.c index 527a2489..b7c6f034 100644 --- a/src/wren_value.c +++ b/src/wren_value.c @@ -140,6 +140,7 @@ ObjFiber* wrenNewFiber(WrenVM* vm, Obj* fn) fiber->openUpvalues = NULL; fiber->caller = NULL; fiber->error = NULL; + fiber->callerIsTrying = false; CallFrame* frame = &fiber->frames[0]; frame->fn = fn; @@ -512,6 +513,8 @@ static void markFiber(WrenVM* vm, ObjFiber* fiber) // The caller. if (fiber->caller != NULL) markFiber(vm, fiber->caller); + if (fiber->error != NULL) markString(vm, fiber->error); + // Keep track of how much memory is still in use. vm->bytesAllocated += sizeof(ObjFiber); // TODO: Count size of error message buffer. @@ -586,13 +589,6 @@ void wrenFreeObj(WrenVM* vm, Obj* obj) wrenMethodBufferClear(vm, &((ObjClass*)obj)->methods); break; - case OBJ_FIBER: - { - ObjFiber* fiber = ((ObjFiber*)obj); - if (fiber->error != NULL) wrenReallocate(vm, fiber->error, 0, 0); - break; - } - case OBJ_FN: { ObjFn* fn = (ObjFn*)obj; @@ -613,6 +609,7 @@ void wrenFreeObj(WrenVM* vm, Obj* obj) break; case OBJ_CLOSURE: + case OBJ_FIBER: case OBJ_INSTANCE: case OBJ_RANGE: case OBJ_UPVALUE: diff --git a/src/wren_value.h b/src/wren_value.h index 2423e06b..8ccfb12c 100644 --- a/src/wren_value.h +++ b/src/wren_value.h @@ -94,6 +94,14 @@ typedef struct DECLARE_BUFFER(Value, Value); +typedef struct +{ + Obj obj; + char* value; + + // TODO: Flexible array. +} ObjString; + // The dynamically allocated data structure for a variable that has been used // by a closure. Whenever a function accesses a variable declared in an // enclosing function, it will get to it through this. @@ -160,7 +168,12 @@ typedef struct sObjFiber // If the fiber failed because of a runtime error, this will contain the // error message. Otherwise, it will be NULL. - char* error; + ObjString* error; + + // This will be true if the caller that called this fiber did so using "try". + // In that case, if this fiber fails with an error, the error will be given + // to the caller. + bool callerIsTrying; } ObjFiber; typedef enum @@ -179,14 +192,6 @@ typedef enum } PrimitiveResult; -typedef struct -{ - Obj obj; - char* value; - - // TODO: Flexible array. -} ObjString; - typedef PrimitiveResult (*Primitive)(WrenVM* vm, ObjFiber* fiber, Value* args); // TODO: See if it's actually a perf improvement to have this in a separate diff --git a/src/wren_vm.c b/src/wren_vm.c index 056b253b..ac23a226 100644 --- a/src/wren_vm.c +++ b/src/wren_vm.c @@ -303,19 +303,30 @@ static void callForeign(WrenVM* vm, ObjFiber* fiber, } // Puts [fiber] into a runtime failed state because of [error]. -static void runtimeError(WrenVM* vm, ObjFiber* fiber, const char* error) +// +// Returns the fiber that should receive the error or `NULL` if no fiber +// caught it. +static ObjFiber* runtimeError(WrenVM* vm, ObjFiber* fiber, const char* error) { - // Copy the error onto the heap. - size_t length = strlen(error) + 1; - char* heapError = wrenReallocate(vm, NULL, 0, length); - strncpy(heapError, error, length); - ASSERT(fiber->error == NULL, "Can only fail once."); - fiber->error = heapError; - // TODO: If the calling fiber is going to handle the error, we shouldn't dump - // a stack trace. + // Store the error in the fiber so it can be accessed later. + fiber->error = AS_STRING(wrenNewString(vm, error, strlen(error))); + + // 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->stack[caller->stackSize - 1] = OBJ_VAL(fiber->error); + + return caller; + } + + // If we got here, nothing caught the error, so show the stack trace. wrenDebugPrintStackTrace(vm, fiber); + return NULL; } // Pushes [function] onto [fiber]'s callstack and invokes it. Expects [numArgs] @@ -384,11 +395,16 @@ static bool runInterpreter(WrenVM* vm) upvalues = ((ObjClosure*)frame->fn)->upvalues; \ } + // Terminates the current fiber with error string [error]. If another calling + // fiber is willing to catch the error, transfers control to it, otherwise + // exits the interpreter. #define RUNTIME_ERROR(error) \ do { \ STORE_FRAME(); \ - runtimeError(vm, fiber, error); \ - return false; \ + fiber = runtimeError(vm, fiber, error); \ + if (fiber == NULL) return false; \ + LOAD_FRAME(); \ + DISPATCH(); \ } \ while (false) @@ -953,6 +969,11 @@ static bool runInterpreter(WrenVM* vm) ObjClass* classObj = wrenNewClass(vm, superclass, numFields, name); + // Don't pop the superclass and name off the stack until the subclass is + // done being created, to make sure it doesn't get collected. + DROP(); + DROP(); + // Now that we know the total number of fields, make sure we don't // overflow. if (superclass->numFields + numFields > MAX_FIELDS) @@ -964,11 +985,6 @@ static bool runInterpreter(WrenVM* vm) RUNTIME_ERROR(message); } - // Don't pop the superclass and name off the stack until the subclass is - // done being created, to make sure it doesn't get collected. - DROP(); - DROP(); - PUSH(OBJ_VAL(classObj)); DISPATCH(); } diff --git a/test/fiber/error.wren b/test/fiber/error.wren new file mode 100644 index 00000000..ccd1e87b --- /dev/null +++ b/test/fiber/error.wren @@ -0,0 +1,7 @@ +var fiber = new Fiber { + "s".unknown +} + +IO.print(fiber.error) // expect: null +IO.print(fiber.try) // expect: String does not implement method 'unknown'. +IO.print(fiber.error) // expect: String does not implement method 'unknown'. diff --git a/test/fiber/is_done_after_error.wren b/test/fiber/is_done_after_error.wren new file mode 100644 index 00000000..506593db --- /dev/null +++ b/test/fiber/is_done_after_error.wren @@ -0,0 +1,6 @@ +var fiber = new Fiber { + "s".unknown +} + +fiber.try +IO.print(fiber.isDone) // expect: true diff --git a/test/fiber/try.wren b/test/fiber/try.wren new file mode 100644 index 00000000..9abfec89 --- /dev/null +++ b/test/fiber/try.wren @@ -0,0 +1,10 @@ +var fiber = new Fiber { + IO.print("before") + true.unknownMethod + IO.print("after") +} + +IO.print(fiber.try) +// expect: before +// expect: Bool does not implement method 'unknownMethod'. +IO.print("after try") // expect: after try diff --git a/test/fiber/try_direct_reenter.wren b/test/fiber/try_direct_reenter.wren new file mode 100644 index 00000000..0dbd2a86 --- /dev/null +++ b/test/fiber/try_direct_reenter.wren @@ -0,0 +1,7 @@ +var fiber + +fiber = new Fiber { + fiber.try // expect runtime error: Fiber has already been called. +} + +fiber.call diff --git a/test/fiber/try_indirect_reenter.wren b/test/fiber/try_indirect_reenter.wren new file mode 100644 index 00000000..36cac039 --- /dev/null +++ b/test/fiber/try_indirect_reenter.wren @@ -0,0 +1,12 @@ +var a +var b + +a = new Fiber { + b.try // expect runtime error: Fiber has already been called. +} + +b = new Fiber { + a.call +} + +b.call diff --git a/test/fiber/try_when_done.wren b/test/fiber/try_when_done.wren new file mode 100644 index 00000000..201d5ca0 --- /dev/null +++ b/test/fiber/try_when_done.wren @@ -0,0 +1,6 @@ +var fiber = new Fiber { + IO.print("try") +} + +fiber.try // expect: try +fiber.try // expect runtime error: Cannot try a finished fiber. diff --git a/test/fiber/try_without_error.wren b/test/fiber/try_without_error.wren new file mode 100644 index 00000000..62e092af --- /dev/null +++ b/test/fiber/try_without_error.wren @@ -0,0 +1,8 @@ +var fiber = new Fiber { + IO.print("fiber") +} + +IO.print("before") // expect: before +IO.print(fiber.try) // expect: fiber + // expect: null +IO.print("after") // expect: after