diff --git a/doc/beta-style inner.txt b/doc/beta-style inner.txt deleted file mode 100644 index c2c5c799..00000000 --- a/doc/beta-style inner.txt +++ /dev/null @@ -1,55 +0,0 @@ -class Base { - foo { - io.write("a") - inner - io.write("b") - } -} - -class Mid is Base {} - -class Derived is Mid { - foo { - io.write("c") - } -} - -var d = Derived.new -d.foo - -// find base-most method "foo" (on Base) -// invoke it -// when inner is hit, walk down inheritance chain to find nearest override -// (in Derived) -// invoke it - - -for every class, store two lists: - -1. list of methods this class defines. -2. mapping of method name to which method body should be invoked first for - that method. this will be the base-most class's definition of a given - method name. - -typedef struct -{ - // The method body to invoke. This will be the base-most definition of a - // method for a given name. - Method* method; - - // If [method] calls `inner`, this is the class whose inner method is invoked. - // If *that* method in turn calls `inner`, we look up the [inner] property of - // this method on that class's dispatch table to chain to the next one. - // - // This is `NULL` if there is no inner method to call. - ClassObj* inner; -} Dispatch; - -typedef struct -{ - Dispatch dispatchTable[MAX_SYMBOLS]; -} ClassObj; - -with normal overridding, can unify those. with inner(), need both. - -whenever a method is invoked, look it up in 2, then call that method body. diff --git a/doc/error-handling.txt b/doc/error-handling.txt new file mode 100644 index 00000000..904cc34a --- /dev/null +++ b/doc/error-handling.txt @@ -0,0 +1,80 @@ +Q: Can we use fibers for error-handling? + +The goal here is to avoid adding support for exception handling if we're already +going to support fibers. A potential bonus would be being able to have +restartable error-handling. + +The general idea is that instead of putting code in a "try" block, you throw it +onto a new fiber. If an error occurs, that fiber is paused, and returns control +back to the spawning fiber. The parent fiber can then decipher the error and +either abandon the fiber, or try to fix the error and resume somehow. + +The first question is what kinds of errors is this useful for. For things like +parsing strings where failure is common and error-handling needs to be +lightweight, I think using fibers is too heavy, both in performance and code. +A better answer there is to lean on dynamic typing and return null on parse +failure. + +On the other hand, it might be nice to be able to resume here if the code that +provided the string is far away and you don't want to have to manually propagate +the error out. + +Programmatic errors like unvalid argument types should halt the fiber but the +programmer will not want to resume that at runtime. Using the mechanism here is +fine since it would then dump a stack trace, etc. But it won't take advantage +of resuming. + +Resuming is probably useful for things like IO errors where the error can't be +easily predicted beforehand but where you may want to handle it gracefully. For +example, if a file can't be opened, the caller may want to wait a while and +try again. + +-- + +After thinking about it, maybe resuming is a bridge too far. Erlang's model is +that a failure just kills the process. I'll note that Erlang does have try and +catch, though. + +The goals for error-handling in a scripting language are: + +0. Have simple semantics and implementation. + +1. Make it easy for developers to track down programmatic errors so they can + fix them. This means bugs like wrong argument types should fail immediately + and loudly, and should provide context (a callstack) about where the error + occurred. + +2. For runtime errors like parsing an invalid string or opening a missing file, + the program should be able to easily detect the error at handle it. + +3. It *may* be useful for programmers to be able to trap all errors and try to + keep the program alive, or at least log the error in a meaningful way. When + you have user-defined scripts, or a lot of code, or code authored by + non-technical people, it's nice if a failure in one part can be reported but + not take down the entire system. + + Two close-at-hand examples: + + - The REPL. A bug in code in the REPL shouldn't kill the whole REPL session. + + - The test framework. In order to write tests in Wren that test programmatic + runtime errors, we need to be able to detect them and output something. + The test runner could just parse the error output when the entire process + dies, but that means you can only have one error test per test file. + +Given those, I'm thinking: + +1. Programmatic errors take down the entire fiber and dump a callstack. + Normally, they will also take down the parent fiber and so on until the + entire program goes down. + +2. Runtime errors return error codes (or null). Things like parsing a string to + a number, etc. should just return an error that you are responsible for + handling. + +3. When handing off control to a fiber, there is a "guarded run" method that + will run the fiber. If it fails with a programmatic error, the invoked fiber + dies, but the parent does not. It gets the callstack and error as some sort + of object it can poke at. + + diff --git a/src/wren_vm.c b/src/wren_vm.c index 26175e3d..bc5f3882 100644 --- a/src/wren_vm.c +++ b/src/wren_vm.c @@ -594,16 +594,15 @@ Value interpret(WrenVM* vm, Value function) #define PUSH(value) (fiber->stack[fiber->stackSize++] = value) #define POP() (fiber->stack[--fiber->stackSize]) #define PEEK() (fiber->stack[fiber->stackSize - 1]) - #define READ_ARG() (frame->ip++, bytecode[ip++]) + #define READ_ARG() (*ip++) // 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 int ip; + register unsigned char* ip; register ObjFn* fn; register Upvalue** upvalues; - register unsigned char* bytecode; // Use this before a CallFrame is pushed to store the local variables back // into the current one. @@ -623,8 +622,7 @@ Value interpret(WrenVM* vm, Value function) { \ fn = AS_CLOSURE(frame->fn)->fn; \ upvalues = AS_CLOSURE(frame->fn)->upvalues; \ - } \ - bytecode = fn->bytecode + } #if WREN_COMPUTED_GOTO @@ -696,11 +694,11 @@ Value interpret(WrenVM* vm, Value function) #define INTERPRET_LOOP DISPATCH(); #define CASE_CODE(name) code_##name - #define DISPATCH() goto *dispatchTable[instruction = bytecode[ip++]] + #define DISPATCH() goto *dispatchTable[instruction = *ip++] #else - #define INTERPRET_LOOP for (;;) switch (instruction = bytecode[ip++]) + #define INTERPRET_LOOP for (;;) switch (instruction = *ip++) #define CASE_CODE(name) case CODE_##name #define DISPATCH() break @@ -711,9 +709,7 @@ Value interpret(WrenVM* vm, Value function) Code instruction; INTERPRET_LOOP { - CASE_CODE(CONSTANT): - PUSH(fn->constants[READ_ARG()]); - DISPATCH(); + CASE_CODE(POP): POP(); DISPATCH(); CASE_CODE(NULL): PUSH(NULL_VAL); DISPATCH(); CASE_CODE(FALSE): PUSH(FALSE_VAL); DISPATCH(); @@ -786,6 +782,24 @@ Value interpret(WrenVM* vm, Value function) DISPATCH(); } + CASE_CODE(LOAD_LOCAL): + { + int local = READ_ARG(); + PUSH(fiber->stack[frame->stackStart + local]); + DISPATCH(); + } + + CASE_CODE(STORE_LOCAL): + { + int local = READ_ARG(); + fiber->stack[frame->stackStart + local] = PEEK(); + DISPATCH(); + } + + CASE_CODE(CONSTANT): + PUSH(fn->constants[READ_ARG()]); + DISPATCH(); + CASE_CODE(SUPER_0): CASE_CODE(SUPER_1): CASE_CODE(SUPER_2): @@ -859,20 +873,6 @@ Value interpret(WrenVM* vm, Value function) DISPATCH(); } - CASE_CODE(LOAD_LOCAL): - { - int local = READ_ARG(); - PUSH(fiber->stack[frame->stackStart + local]); - DISPATCH(); - } - - CASE_CODE(STORE_LOCAL): - { - int local = READ_ARG(); - fiber->stack[frame->stackStart + local] = PEEK(); - DISPATCH(); - } - CASE_CODE(LOAD_UPVALUE): { ASSERT(upvalues != NULL, @@ -934,7 +934,6 @@ Value interpret(WrenVM* vm, Value function) } CASE_CODE(DUP): PUSH(PEEK()); DISPATCH(); - CASE_CODE(POP): POP(); DISPATCH(); CASE_CODE(JUMP): { @@ -957,6 +956,7 @@ Value interpret(WrenVM* vm, Value function) Value condition = POP(); // False is the only falsey value. + // TODO: Null should be falsey too. if (IS_FALSE(condition)) ip += offset; DISPATCH(); } @@ -967,6 +967,7 @@ Value interpret(WrenVM* vm, Value function) Value condition = PEEK(); // False is the only falsey value. + // TODO: Null should be falsey too. if (!IS_FALSE(condition)) { // Discard the condition and evaluate the right hand side. @@ -986,6 +987,7 @@ Value interpret(WrenVM* vm, Value function) Value condition = PEEK(); // False is the only falsey value. + // TODO: Null should be falsey too. if (IS_FALSE(condition)) { // Discard the condition and evaluate the right hand side. @@ -1155,9 +1157,19 @@ Value interpret(WrenVM* vm, Value function) void wrenCallFunction(Fiber* fiber, Value function, int numArgs) { // TODO: Check for stack overflow. - fiber->frames[fiber->numFrames].fn = function; - fiber->frames[fiber->numFrames].ip = 0; - fiber->frames[fiber->numFrames].stackStart = fiber->stackSize - numArgs; + CallFrame* frame = &fiber->frames[fiber->numFrames]; + frame->fn = function; + frame->stackStart = fiber->stackSize - numArgs; + + frame->ip = 0; + if (IS_FN(function)) + { + frame->ip = AS_FN(function)->bytecode; + } + else + { + frame->ip = AS_CLOSURE(function)->fn->bytecode; + } fiber->numFrames++; } diff --git a/src/wren_vm.h b/src/wren_vm.h index 83c4aa63..5e833560 100644 --- a/src/wren_vm.h +++ b/src/wren_vm.h @@ -244,9 +244,9 @@ struct WrenVM // TODO: Move into wren_vm.c. typedef struct { - // Index of the current (really next-to-be-executed) instruction in the - // block's bytecode. - int ip; + // Pointer to the current (really next-to-be-executed) instruction in the + // function's bytecode. + unsigned char* ip; // The function or closure being executed. Value fn;