forked from Mirror/wren
Make ip an actual pointer.
This commit is contained in:
@ -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.
|
||||
80
doc/error-handling.txt
Normal file
80
doc/error-handling.txt
Normal file
@ -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.
|
||||
|
||||
|
||||
@ -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++;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user