Overhaul exceptions with tracebacks; 'except Type...'
This commit is contained in:
parent
b527561b53
commit
9bbb0a1d6e
@ -49,6 +49,7 @@ typedef enum {
|
||||
OP_SUBTRACT,
|
||||
OP_SWAP,
|
||||
OP_TRUE,
|
||||
OP_FILTER_EXCEPT,
|
||||
|
||||
OP_CALL = 64,
|
||||
OP_CLASS,
|
||||
|
@ -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() {
|
||||
|
@ -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("<unnamed>"));
|
||||
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("<function >");
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
148
src/vm.c
148
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
|
||||
|
17
test/testExceptionTracebacks.krk
Normal file
17
test/testExceptionTracebacks.krk
Normal file
@ -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"))
|
||||
|
7
test/testExceptionTracebacks.krk.expect
Normal file
7
test/testExceptionTracebacks.krk.expect
Normal file
@ -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!
|
14
test/testFilteredExceptions.krk
Normal file
14
test/testFilteredExceptions.krk
Normal file
@ -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"))
|
||||
|
3
test/testFilteredExceptions.krk.expect
Normal file
3
test/testFilteredExceptions.krk.expect
Normal file
@ -0,0 +1,3 @@
|
||||
Caught a TypeError('A type error')
|
||||
Caught a ValueError('A value error')
|
||||
That's a name error!
|
Loading…
Reference in New Issue
Block a user