From 9bbb0a1d6e282b731ab8ef082663e209845b5806 Mon Sep 17 00:00:00 2001 From: "K. Lange" Date: Thu, 18 Feb 2021 11:04:59 +0900 Subject: [PATCH] Overhaul exceptions with tracebacks; 'except Type...' --- src/chunk.h | 1 + src/compiler.c | 18 ++- src/obj_function.c | 20 +++- src/opcodes.h | 1 + src/vm.c | 148 ++++++++++++++++++------ test/testExceptionTracebacks.krk | 17 +++ test/testExceptionTracebacks.krk.expect | 7 ++ test/testFilteredExceptions.krk | 14 +++ test/testFilteredExceptions.krk.expect | 3 + 9 files changed, 188 insertions(+), 41 deletions(-) create mode 100644 test/testExceptionTracebacks.krk create mode 100644 test/testExceptionTracebacks.krk.expect create mode 100644 test/testFilteredExceptions.krk create mode 100644 test/testFilteredExceptions.krk.expect diff --git a/src/chunk.h b/src/chunk.h index b61b3e1..703a786 100644 --- a/src/chunk.h +++ b/src/chunk.h @@ -49,6 +49,7 @@ typedef enum { OP_SUBTRACT, OP_SWAP, OP_TRUE, + OP_FILTER_EXCEPT, OP_CALL = 64, OP_CLASS, diff --git a/src/compiler.c b/src/compiler.c index a7711af..b856c7c 100644 --- a/src/compiler.c +++ b/src/compiler.c @@ -191,7 +191,7 @@ static void and_(int canAssign); static KrkToken classDeclaration(); static void declareVariable(); static void namedVariable(KrkToken name, int canAssign); -static void addLocal(KrkToken name); +static Local * addLocal(KrkToken name); static void string(int canAssign); static KrkToken decorator(size_t level, FunctionType type); static void call(int canAssign); @@ -1536,7 +1536,8 @@ static void tryStatement() { /* Make sure we are in a local scope so this ends up on the stack */ beginScope(); int tryJump = emitJump(OP_PUSH_TRY); - addLocal(syntheticToken("exception")); + /* We'll rename this later, but it needs to be on the stack now as it represents the exception handler */ + Local * exceptionObject = addLocal(syntheticToken("")); defineVariable(0); beginScope(); @@ -1553,6 +1554,16 @@ static void tryStatement() { advance(); } if (match(TOKEN_EXCEPT)) { + if (!check(TOKEN_COLON) && !check(TOKEN_AS)) { + expression(); + emitByte(OP_FILTER_EXCEPT); + } + if (match(TOKEN_AS)) { + consume(TOKEN_IDENTIFIER, "Expected identifier after 'as'"); + exceptionObject->name = parser.previous; + } else { + exceptionObject->name = syntheticToken("exception"); + } consume(TOKEN_COLON, "Expect ':' after except."); beginScope(); block(blockWidth,"except"); @@ -2428,7 +2439,7 @@ static ssize_t resolveLocal(Compiler * compiler, KrkToken * name) { return -1; } -static void addLocal(KrkToken name) { +static Local * addLocal(KrkToken name) { if (current->localCount + 1 > current->localsSpace) { size_t old = current->localsSpace; current->localsSpace = GROW_CAPACITY(old); @@ -2449,6 +2460,7 @@ static void addLocal(KrkToken name) { current->function->localNames[current->function->localNameCount].deathday = 0; current->function->localNames[current->function->localNameCount].name = krk_copyString(name.start, name.length); current->function->localNameCount++; + return local; } static void declareVariable() { diff --git a/src/obj_function.c b/src/obj_function.c index acb2707..3019747 100644 --- a/src/obj_function.c +++ b/src/obj_function.c @@ -3,6 +3,7 @@ #include "value.h" #include "memory.h" #include "util.h" +#include "debug.h" /* function.__doc__ */ static KrkValue _closure_get_doc(int argc, KrkValue argv[], int hasKw) { @@ -24,13 +25,15 @@ static KrkValue _bound_get_doc(int argc, KrkValue argv[], int hasKw) { /* Check for and return the name of a native function as a string object */ static KrkValue nativeFunctionName(KrkValue func) { const char * string = ((KrkNative*)AS_OBJECT(func))->name; + if (!string) return OBJECT_VAL(S("")); size_t len = strlen(string); return OBJECT_VAL(krk_copyString(string,len)); } /* function.__name__ */ static KrkValue _closure_get_name(int argc, KrkValue argv[], int hasKw) { - if (!IS_CLOSURE(argv[0])) return nativeFunctionName(argv[0]); + if (IS_NATIVE(argv[0])) return nativeFunctionName(argv[0]); + else if (!IS_CLOSURE(argv[0])) return krk_runtimeError(vm.exceptions->typeError, "'%s' is neither a closure nor a native", krk_typeName(argv[0])); return AS_CLOSURE(argv[0])->function->name ? OBJECT_VAL(AS_CLOSURE(argv[0])->function->name) : OBJECT_VAL(S("")); } @@ -40,9 +43,22 @@ static KrkValue _bound_get_name(int argc, KrkValue argv[], int hasKw) { return _closure_get_name(1, (KrkValue[]){OBJECT_VAL(boundMethod->method)}, hasKw); } +static KrkValue _closure_ip_to_line(int argc, KrkValue argv[], int hasKw) { + if (argc < 2) return krk_runtimeError(vm.exceptions->argumentError, "%s() expects exactly 2 arguments", "ip_to_line"); + if (!IS_CLOSURE(argv[0])) return NONE_VAL(); + if (!IS_INTEGER(argv[1])) return TYPE_ERROR(int,argv[1]); + return INTEGER_VAL(krk_lineNumber(&AS_CLOSURE(argv[0])->function->chunk, AS_INTEGER(argv[1]))); +} + +static KrkValue _bound_ip_to_line(int argc, KrkValue argv[], int hasKw) { + KrkBoundMethod * boundMethod = AS_BOUND_METHOD(argv[0]); + return _closure_ip_to_line(1, (KrkValue[]){OBJECT_VAL(boundMethod->method)}, hasKw); +} + /* function.__str__ / function.__repr__ */ static KrkValue _closure_str(int argc, KrkValue argv[], int hasKw) { KrkValue s = _closure_get_name(argc, argv, hasKw); + if (!IS_STRING(s)) return NONE_VAL(); krk_push(s); size_t len = AS_STRING(s)->length + sizeof(""); @@ -114,6 +130,7 @@ void _createAndBind_functionClass(void) { krk_defineNative(&vm.baseClasses->functionClass->methods, ":__name__", _closure_get_name); krk_defineNative(&vm.baseClasses->functionClass->methods, ":__file__", _closure_get_file); krk_defineNative(&vm.baseClasses->functionClass->methods, ":__args__", _closure_get_argnames); + krk_defineNative(&vm.baseClasses->functionClass->methods, "_ip_to_line", _closure_ip_to_line); krk_finalizeClass(vm.baseClasses->functionClass); ADD_BASE_CLASS(vm.baseClasses->methodClass, "method", vm.baseClasses->objectClass); @@ -123,5 +140,6 @@ void _createAndBind_functionClass(void) { krk_defineNative(&vm.baseClasses->methodClass->methods, ":__name__", _bound_get_name); krk_defineNative(&vm.baseClasses->methodClass->methods, ":__file__", _bound_get_file); krk_defineNative(&vm.baseClasses->methodClass->methods, ":__args__", _bound_get_argnames); + krk_defineNative(&vm.baseClasses->methodClass->methods, "_ip_to_line", _bound_ip_to_line); krk_finalizeClass(vm.baseClasses->methodClass); } diff --git a/src/opcodes.h b/src/opcodes.h index bdca51a..6516d41 100644 --- a/src/opcodes.h +++ b/src/opcodes.h @@ -36,6 +36,7 @@ SIMPLE(OP_IS) SIMPLE(OP_POW) SIMPLE(OP_CREATE_PROPERTY) SIMPLE(OP_CLEANUP_WITH) +SIMPLE(OP_FILTER_EXCEPT) CONSTANT(OP_DEFINE_GLOBAL,(void)0) CONSTANT(OP_CONSTANT,(void)0) CONSTANT(OP_GET_GLOBAL,(void)0) diff --git a/src/vm.c b/src/vm.c index cc79f98..ac81782 100644 --- a/src/vm.c +++ b/src/vm.c @@ -167,47 +167,66 @@ static void dumpStack(CallFrame * frame) { * the source file and print the corresponding line. */ void krk_dumpTraceback() { - if (krk_currentThread.frameCount) { - fprintf(stderr, "Traceback (most recent call last):\n"); - for (size_t i = 0; i <= krk_currentThread.frameCount - 1; i++) { - CallFrame * frame = &krk_currentThread.frames[i]; - KrkFunction * function = frame->closure->function; - size_t instruction = frame->ip - function->chunk.code - 1; - int lineNo = (int)krk_lineNumber(&function->chunk, instruction); - fprintf(stderr, " File \"%s\", line %d, in %s\n", - (function->chunk.filename ? function->chunk.filename->chars : "?"), - lineNo, - (function->name ? function->name->chars : "(unnamed)")); - if (function->chunk.filename) { - FILE * f = fopen(function->chunk.filename->chars, "r"); - if (f) { - int line = 1; - do { - char c = fgetc(f); - if (c < -1) break; - if (c == '\n') { - line++; - continue; - } - if (line == lineNo) { - fprintf(stderr," "); - while (c == ' ') c = fgetc(f); - do { - fputc(c, stderr); - c = fgetc(f); - } while (!feof(f) && c > 0 && c != '\n'); - fprintf(stderr, "\n"); - break; - } - } while (!feof(f)); - fclose(f); + if (!krk_valuesEqual(krk_currentThread.currentException,NONE_VAL())) { + krk_push(krk_currentThread.currentException); + KrkValue tracebackEntries; + if (IS_INSTANCE(krk_currentThread.currentException) + && krk_tableGet(&AS_INSTANCE(krk_currentThread.currentException)->fields, OBJECT_VAL(S("traceback")), &tracebackEntries) + && IS_list(tracebackEntries) && AS_LIST(tracebackEntries)->count > 0) { + /* This exception has a traceback we can print. */ + fprintf(stderr, "Traceback (most recent call last):\n"); + for (size_t i = 0; i < AS_LIST(tracebackEntries)->count; ++i) { + + /* Quietly skip invalid entries as we don't want to bother printing explanatory text for them */ + if (!IS_TUPLE(AS_LIST(tracebackEntries)->values[i])) continue; + KrkTuple * entry = AS_TUPLE(AS_LIST(tracebackEntries)->values[i]); + if (entry->values.count != 2) continue; + if (!IS_CLOSURE(entry->values.values[0])) continue; + if (!IS_INTEGER(entry->values.values[1])) continue; + + /* Get the function and instruction index from this traceback entry */ + KrkClosure * closure = AS_CLOSURE(entry->values.values[0]); + KrkFunction * function = closure->function; + size_t instruction = AS_INTEGER(entry->values.values[1]); + + /* Calculate the line number */ + int lineNo = (int)krk_lineNumber(&function->chunk, instruction); + + /* Print the simple stuff that we already know */ + fprintf(stderr, " File \"%s\", line %d, in %s\n", + (function->chunk.filename ? function->chunk.filename->chars : "?"), + lineNo, + (function->name ? function->name->chars : "(unnamed)")); + + /* Try to open the file */ + if (function->chunk.filename) { + FILE * f = fopen(function->chunk.filename->chars, "r"); + if (f) { + int line = 1; + do { + char c = fgetc(f); + if (c < -1) break; + if (c == '\n') { + line++; + continue; + } + if (line == lineNo) { + fprintf(stderr," "); + while (c == ' ') c = fgetc(f); + do { + fputc(c, stderr); + c = fgetc(f); + } while (!feof(f) && c > 0 && c != '\n'); + fprintf(stderr, "\n"); + break; + } + } while (!feof(f)); + fclose(f); + } } } } - } - if (!krk_valuesEqual(krk_currentThread.currentException,NONE_VAL())) { - krk_push(krk_currentThread.currentException); /* Is this a SyntaxError? Handle those specially. */ if (krk_isInstanceOf(krk_currentThread.currentException, vm.exceptions->syntaxError)) { KrkValue result = krk_callSimple(OBJECT_VAL(krk_getType(krk_currentThread.currentException)->_tostr), 1, 0); @@ -228,6 +247,37 @@ void krk_dumpTraceback() { } } +/** + * Attach a traceback to the current exception object, if it doesn't already have one. + */ +static void attachTraceback(void) { + if (IS_INSTANCE(krk_currentThread.currentException)) { + KrkInstance * theException = AS_INSTANCE(krk_currentThread.currentException); + + /* If there already is a traceback, don't add a new one; this exception was re-raised. */ + KrkValue existing; + if (krk_tableGet(&theException->fields, OBJECT_VAL(S("traceback")), &existing)) return; + + KrkValue tracebackList = krk_list_of(0,NULL,0); + krk_push(tracebackList); + /* Build the traceback object */ + if (krk_currentThread.frameCount) { + for (size_t i = 0; i < krk_currentThread.frameCount; i++) { + CallFrame * frame = &krk_currentThread.frames[i]; + KrkTuple * tbEntry = krk_newTuple(2); + krk_push(OBJECT_VAL(tbEntry)); + tbEntry->values.values[tbEntry->values.count++] = OBJECT_VAL(frame->closure); + tbEntry->values.values[tbEntry->values.count++] = INTEGER_VAL(frame->ip - frame->closure->function->chunk.code - 1); + krk_tupleUpdateHash(tbEntry); + krk_writeValueArray(AS_LIST(tracebackList), OBJECT_VAL(tbEntry)); + krk_pop(); + } + } + krk_attachNamedValue(&theException->fields, "traceback", tracebackList); + krk_pop(); + } /* else: probably a legacy 'raise str', just don't bother. */ +} + /** * Raise an exception. Creates an exception object of the requested type * and formats a message string to attach to it. Exception classes are @@ -254,6 +304,7 @@ KrkValue krk_runtimeError(KrkClass * type, const char * fmt, ...) { /* Set the current exception to be picked up by handleException */ krk_currentThread.currentException = OBJECT_VAL(exceptionObject); + attachTraceback(); return NONE_VAL(); } @@ -1831,6 +1882,7 @@ static KrkValue run() { case OP_POP: krk_pop(); break; case OP_RAISE: { krk_currentThread.currentException = krk_pop(); + attachTraceback(); krk_currentThread.flags |= KRK_HAS_EXCEPTION; goto _finishException; } @@ -1945,6 +1997,28 @@ static KrkValue run() { krk_push(OBJECT_VAL(newProperty)); break; } + case OP_FILTER_EXCEPT: { + int isMatch = 0; + if (IS_CLASS(krk_peek(0)) && krk_isInstanceOf(krk_peek(1), AS_CLASS(krk_peek(0)))) { + isMatch = 1; + } if (IS_TUPLE(krk_peek(0))) { + for (size_t i = 0; i < AS_TUPLE(krk_peek(0))->values.count; ++i) { + if (IS_CLASS(AS_TUPLE(krk_peek(0))->values.values[i]) && krk_isInstanceOf(krk_peek(1), AS_CLASS(AS_TUPLE(krk_peek(0))->values.values[i]))) { + isMatch = 1; + break; + } + } + } + if (!isMatch) { + /* Restore and re-raise the exception if it didn't match. */ + krk_currentThread.currentException = krk_peek(1); + krk_currentThread.flags |= KRK_HAS_EXCEPTION; + goto _finishException; + } + /* Else pop the filter value */ + krk_pop(); + break; + } /* * Two-byte operands diff --git a/test/testExceptionTracebacks.krk b/test/testExceptionTracebacks.krk new file mode 100644 index 0000000..b87a017 --- /dev/null +++ b/test/testExceptionTracebacks.krk @@ -0,0 +1,17 @@ +def doTheThing(excp): + try: + try: + raise excp + except (TypeError, ValueError): + print("Caught a", repr(exception)) + for i in exception.traceback: + let func, instr = i + print(f" File '{func.__file__}', line {func._ip_to_line(instr)}, in {func.__name__}") + except NameError: + print("That's a name error!") + + +doTheThing(TypeError("A type error")) +doTheThing(ValueError("A value error")) +doTheThing(NameError("A name error")) + diff --git a/test/testExceptionTracebacks.krk.expect b/test/testExceptionTracebacks.krk.expect new file mode 100644 index 0000000..5e156d1 --- /dev/null +++ b/test/testExceptionTracebacks.krk.expect @@ -0,0 +1,7 @@ +Caught a TypeError('A type error') + File 'test/testExceptionTracebacks.krk', line 14, in __main__ + File 'test/testExceptionTracebacks.krk', line 4, in doTheThing +Caught a ValueError('A value error') + File 'test/testExceptionTracebacks.krk', line 15, in __main__ + File 'test/testExceptionTracebacks.krk', line 4, in doTheThing +That's a name error! diff --git a/test/testFilteredExceptions.krk b/test/testFilteredExceptions.krk new file mode 100644 index 0000000..73275c1 --- /dev/null +++ b/test/testFilteredExceptions.krk @@ -0,0 +1,14 @@ +def doTheThing(excp): + try: + try: + raise excp + except (TypeError, ValueError) as e: + print("Caught a", repr(e)) + except NameError: + print("That's a name error!") + + +doTheThing(TypeError("A type error")) +doTheThing(ValueError("A value error")) +doTheThing(NameError("A name error")) + diff --git a/test/testFilteredExceptions.krk.expect b/test/testFilteredExceptions.krk.expect new file mode 100644 index 0000000..7221b2b --- /dev/null +++ b/test/testFilteredExceptions.krk.expect @@ -0,0 +1,3 @@ +Caught a TypeError('A type error') +Caught a ValueError('A value error') +That's a name error!