Skip to content

Implement chained exceptions #7069

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ msgstr ""
msgid "%q in use"
msgstr ""

#: py/obj.c py/objstr.c py/objstrunicode.c
#: py/objstr.c py/objstrunicode.c
msgid "%q index out of range"
msgstr ""

Expand Down Expand Up @@ -171,11 +171,11 @@ msgstr ""
msgid "%q must be an int"
msgstr ""

#: py/argcheck.c
#: py/argcheck.c py/obj.c
msgid "%q must be of type %q"
msgstr ""

#: shared-bindings/digitalio/Pull.c
#: py/objexcept.c shared-bindings/digitalio/Pull.c
msgid "%q must be of type %q or None"
msgstr ""

Expand Down Expand Up @@ -894,6 +894,10 @@ msgstr ""
msgid "Drive mode not used when direction is input."
msgstr ""

#: py/obj.c
msgid "During handling of the above exception, another exception occurred:"
msgstr ""

#: shared-bindings/aesio/aes.c
msgid "ECB only operates on 16 bytes at a time"
msgstr ""
Expand Down Expand Up @@ -2007,6 +2011,10 @@ msgid ""
"exit safe mode."
msgstr ""

#: py/obj.c
msgid "The above exception was the direct cause of the following exception:"
msgstr ""

#: ports/espressif/boards/m5stack_atom_lite/mpconfigboard.h
msgid "The central button was pressed at start up.\n"
msgstr ""
Expand Down Expand Up @@ -3153,7 +3161,7 @@ msgid "index is out of bounds"
msgstr ""

#: extmod/ulab/code/numpy/numerical.c extmod/ulab/code/ulab_tools.c
#: ports/espressif/common-hal/pulseio/PulseIn.c py/obj.c
#: ports/espressif/common-hal/pulseio/PulseIn.c
#: shared-bindings/bitmaptools/__init__.c
msgid "index out of range"
msgstr ""
Expand Down Expand Up @@ -3336,10 +3344,6 @@ msgstr ""
msgid "invalid syntax for number"
msgstr ""

#: py/objexcept.c
msgid "invalid traceback"
msgstr ""

#: py/objtype.c
msgid "issubclass() arg 1 must be a class"
msgstr ""
Expand Down
1 change: 1 addition & 0 deletions ports/unix/variants/coverage/mpconfigvariant.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
#define MICROPY_PY_UCRYPTOLIB (1)
#define MICROPY_PY_UCRYPTOLIB_CTR (1)
#define MICROPY_PY_MICROPYTHON_HEAP_LOCKED (1)
#define MICROPY_CPYTHON_EXCEPTION_CHAIN (1)

// use vfs's functions for import stat and builtin open
#define mp_import_stat mp_vfs_import_stat
Expand Down
3 changes: 3 additions & 0 deletions py/circuitpy_mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ typedef long mp_off_t;
#ifndef MICROPY_CPYTHON_COMPAT
#define MICROPY_CPYTHON_COMPAT (CIRCUITPY_FULL_BUILD)
#endif
#ifndef MICROPY_CPYTHON_EXCEPTION_CHAIN
#define MICROPY_CPYTHON_EXCEPTION_CHAIN (CIRCUITPY_FULL_BUILD)
#endif
#define MICROPY_PY_BUILTINS_POW3 (CIRCUITPY_BUILTINS_POW3)
#define MICROPY_PY_FSTRINGS (1)
#define MICROPY_MODULE_WEAK_LINKS (0)
Expand Down
10 changes: 10 additions & 0 deletions py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,16 @@ typedef long long mp_longint_impl_t;
#define MICROPY_WARNINGS (0)
#endif

// Whether to support chained exceptions
#ifndef MICROPY_CPYTHON_EXCEPTION_CHAIN
#define MICROPY_CPYTHON_EXCEPTION_CHAIN (0)
#endif

// Whether the statically allocated GeneratorExit exception may be const
#ifndef MICROPY_CONST_GENERATOREXIT_OBJ
#define MICROPY_CONST_GENERATOREXIT_OBJ (!MICROPY_CPYTHON_EXCEPTION_CHAIN)
#endif

// Whether to support warning categories
#ifndef MICROPY_WARNINGS_CATEGORY
#define MICROPY_WARNINGS_CATEGORY (0)
Expand Down
24 changes: 24 additions & 0 deletions py/obj.c
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,33 @@ void mp_obj_print(mp_obj_t o_in, mp_print_kind_t kind) {
mp_obj_print_helper(MP_PYTHON_PRINTER, o_in, kind);
}

static void mp_obj_print_inner_exception(const mp_print_t *print, mp_obj_t self_in, mp_int_t limit) {
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
mp_obj_exception_t *self = mp_obj_exception_get_native(self_in);
const compressed_string_t *msg = MP_ERROR_TEXT("During handling of the above exception, another exception occurred:");
mp_obj_exception_t *inner = NULL;
if (self->cause) {
msg = MP_ERROR_TEXT("The above exception was the direct cause of the following exception:");
inner = self->cause;
} else if (!self->suppress_context) {
inner = self->context;
}
if (inner && !inner->marked) {
inner->marked = true;
mp_obj_print_exception_with_limit(print, MP_OBJ_FROM_PTR(inner), limit);
inner->marked = false;
mp_printf(print, "\n");
mp_cprintf(print, msg);
mp_printf(print, "\n\n");
}
#endif
}

// helper function to print an exception with traceback
void mp_obj_print_exception_with_limit(const mp_print_t *print, mp_obj_t exc, mp_int_t limit) {
if (mp_obj_is_exception_instance(exc) && stack_ok()) {
mp_obj_print_inner_exception(print, exc, limit);

size_t n, *values;
mp_obj_exception_get_traceback(exc, &n, &values);
if (n > 0) {
Expand Down
4 changes: 3 additions & 1 deletion py/obj.h
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,9 @@ extern const struct _mp_obj_dict_t mp_const_empty_dict_obj;
extern const struct _mp_obj_traceback_t mp_const_empty_traceback_obj;
extern const struct _mp_obj_singleton_t mp_const_ellipsis_obj;
extern const struct _mp_obj_singleton_t mp_const_notimplemented_obj;
extern const struct _mp_obj_exception_t mp_const_GeneratorExit_obj;
#if MICROPY_CONST_GENERATOREXIT_OBJ
extern const struct _mp_obj_exception_t mp_static_GeneratorExit_obj;
#endif

// Fixed empty map. Useful when calling keyword-receiving functions
// without any keywords from C, etc.
Expand Down
38 changes: 36 additions & 2 deletions py/objexcept.c
Original file line number Diff line number Diff line change
Expand Up @@ -218,19 +218,45 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
mp_obj_exception_t *self = MP_OBJ_TO_PTR(self_in);
if (dest[0] != MP_OBJ_NULL) {
// store/delete attribute
if (self == &mp_const_GeneratorExit_obj) {
#if MICROPY_CONST_GENERATOREXIT_OBJ
if (self == &mp_static_GeneratorExit_obj) {
mp_raise_AttributeError(MP_ERROR_TEXT("can't set attribute"));
}
#endif
if (attr == MP_QSTR___traceback__) {
if (dest[1] == mp_const_none) {
self->traceback = (mp_obj_traceback_t *)&mp_const_empty_traceback_obj;
} else {
if (!mp_obj_is_type(dest[1], &mp_type_traceback)) {
mp_raise_TypeError(MP_ERROR_TEXT("invalid traceback"));
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), MP_QSTR___context__, MP_QSTR_traceback);
}
self->traceback = MP_OBJ_TO_PTR(dest[1]);
}
dest[0] = MP_OBJ_NULL; // indicate success
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
} else if (attr == MP_QSTR___cause__) {
if (dest[1] == mp_const_none) {
self->cause = NULL;
} else if (!mp_obj_is_type(dest[1], &mp_type_BaseException)) {
self->cause = dest[1];
} else {
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), attr, MP_QSTR_BaseException);
}
self->suppress_context = true;
dest[0] = MP_OBJ_NULL; // indicate success
} else if (attr == MP_QSTR___context__) {
if (dest[1] == mp_const_none) {
self->context = NULL;
} else if (!mp_obj_is_type(dest[1], &mp_type_BaseException)) {
self->context = dest[1];
} else {
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or None"), attr, MP_QSTR_BaseException);
}
dest[0] = MP_OBJ_NULL; // indicate success
} else if (attr == MP_QSTR___suppress_context__) {
self->suppress_context = mp_obj_is_true(dest[1]);
dest[0] = MP_OBJ_NULL; // indicate success
#endif
}
return;
}
Expand All @@ -240,6 +266,14 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
dest[0] = mp_obj_exception_get_value(self_in);
} else if (attr == MP_QSTR___traceback__) {
dest[0] = (self->traceback) ? MP_OBJ_FROM_PTR(self->traceback) : mp_const_none;
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
} else if (attr == MP_QSTR___cause__) {
dest[0] = (self->cause) ? MP_OBJ_FROM_PTR(self->cause) : mp_const_none;
} else if (attr == MP_QSTR___context__) {
dest[0] = (self->context) ? MP_OBJ_FROM_PTR(self->context) : mp_const_none;
} else if (attr == MP_QSTR___suppress_context__) {
dest[0] = mp_obj_new_bool(self->suppress_context);
#endif
#if MICROPY_CPYTHON_COMPAT
} else if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(self->base.type), MP_OBJ_FROM_PTR(&mp_type_OSError))) {
if (attr == MP_QSTR_errno) {
Expand Down
5 changes: 5 additions & 0 deletions py/objexcept.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ typedef struct _mp_obj_exception_t {
mp_obj_base_t base;
mp_obj_tuple_t *args;
mp_obj_traceback_t *traceback;
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
struct _mp_obj_exception_t *cause, *context;
bool suppress_context;
bool marked;
#endif
} mp_obj_exception_t;

void mp_obj_exception_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind);
Expand Down
19 changes: 17 additions & 2 deletions py/objgenerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@
#include "supervisor/shared/translate/translate.h"

// Instance of GeneratorExit exception - needed by generator.close()
const mp_obj_exception_t mp_const_GeneratorExit_obj = {{&mp_type_GeneratorExit}, (mp_obj_tuple_t *)&mp_const_empty_tuple_obj, (mp_obj_traceback_t *)&mp_const_empty_traceback_obj};
#if MICROPY_CONST_GENERATOREXIT_OBJ
const
#else
static
#endif
mp_obj_exception_t mp_static_GeneratorExit_obj = {{&mp_type_GeneratorExit}, (mp_obj_tuple_t *)&mp_const_empty_tuple_obj, (mp_obj_traceback_t *)&mp_const_empty_traceback_obj};

/******************************************************************************/
/* generator wrapper */
Expand Down Expand Up @@ -362,9 +367,19 @@ STATIC mp_obj_t gen_instance_throw(size_t n_args, const mp_obj_t *args) {
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(gen_instance_throw_obj, 2, 4, gen_instance_throw);

static mp_obj_t generatorexit(void) {
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
MP_STATIC_ASSERT(!MICROPY_CONST_GENERATOREXIT_OBJ);
mp_static_GeneratorExit_obj.context = NULL;
mp_static_GeneratorExit_obj.cause = NULL;
mp_static_GeneratorExit_obj.suppress_context = false;
#endif
return MP_OBJ_FROM_PTR(&mp_static_GeneratorExit_obj);
}

STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {
mp_obj_t ret;
switch (mp_obj_gen_resume(self_in, mp_const_none, MP_OBJ_FROM_PTR(&mp_const_GeneratorExit_obj), &ret)) {
switch (mp_obj_gen_resume(self_in, mp_const_none, generatorexit(), &ret)) {
case MP_VM_RETURN_YIELD:
mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("generator ignored GeneratorExit"));

Expand Down
53 changes: 42 additions & 11 deletions py/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,15 @@
#define TRACE_TICK(current_ip, current_sp, is_exception)
#endif // MICROPY_PY_SYS_SETTRACE

STATIC mp_obj_t get_active_exception(mp_exc_stack_t *exc_sp, mp_exc_stack_t *exc_stack) {
for (mp_exc_stack_t *e = exc_sp; e >= exc_stack; --e) {
if (e->prev_exc != NULL) {
return MP_OBJ_FROM_PTR(e->prev_exc);
}
}
return MP_OBJ_NULL;
}

// fastn has items in reverse order (fastn[0] is local[0], fastn[-1] is local[1], etc)
// sp points to bottom of stack which grows up
// returns:
Expand Down Expand Up @@ -1129,13 +1138,7 @@ unwind_jump:;
ENTRY(MP_BC_RAISE_LAST): {
MARK_EXC_IP_SELECTIVE();
// search for the inner-most previous exception, to reraise it
mp_obj_t obj = MP_OBJ_NULL;
for (mp_exc_stack_t *e = exc_sp; e >= exc_stack; --e) {
if (e->prev_exc != NULL) {
obj = MP_OBJ_FROM_PTR(e->prev_exc);
break;
}
}
mp_obj_t obj = get_active_exception(exc_sp, exc_stack);
if (obj == MP_OBJ_NULL) {
obj = mp_obj_new_exception_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("no active exception to reraise"));
}
Expand All @@ -1145,14 +1148,30 @@ unwind_jump:;
ENTRY(MP_BC_RAISE_OBJ): {
MARK_EXC_IP_SELECTIVE();
mp_obj_t obj = mp_make_raise_obj(TOP());
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack);
if (active_exception != MP_OBJ_NULL) {
mp_store_attr(obj, MP_QSTR___context__, active_exception);
}
#endif
RAISE(obj);
}

ENTRY(MP_BC_RAISE_FROM): {
MARK_EXC_IP_SELECTIVE();
mp_warning(NULL, "exception chaining not supported");
sp--; // ignore (pop) "from" argument
mp_obj_t cause = POP();
mp_obj_t obj = mp_make_raise_obj(TOP());
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
// search for the inner-most previous exception, to chain it
mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack);
if (active_exception != MP_OBJ_NULL) {
mp_store_attr(obj, MP_QSTR___context__, active_exception);
}
mp_store_attr(obj, MP_QSTR___cause__, cause);
#else
(void)cause;
mp_warning(NULL, "exception chaining not supported");
#endif
RAISE(obj);
}

Expand Down Expand Up @@ -1391,7 +1410,10 @@ unwind_jump:;
// - constant GeneratorExit object, because it's const
// - exceptions re-raised by END_FINALLY
// - exceptions re-raised explicitly by "raise"
if (nlr.ret_val != &mp_const_GeneratorExit_obj
if ( true
#if MICROPY_CONST_GENERATOREXIT_OBJ
&& nlr.ret_val != &mp_static_GeneratorExit_obj
#endif
&& *code_state->ip != MP_BC_END_FINALLY
&& *code_state->ip != MP_BC_RAISE_LAST) {
const byte *ip = code_state->fun_bc->bytecode;
Expand Down Expand Up @@ -1434,10 +1456,19 @@ unwind_jump:;
// catch exception and pass to byte code
code_state->ip = exc_sp->handler;
mp_obj_t *sp = MP_TAGPTR_PTR(exc_sp->val_sp);
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
mp_obj_t active_exception = get_active_exception(exc_sp, exc_stack);
#endif
// save this exception in the stack so it can be used in a reraise, if needed
exc_sp->prev_exc = nlr.ret_val;
mp_obj_t obj = MP_OBJ_FROM_PTR(nlr.ret_val);
#if MICROPY_CPYTHON_EXCEPTION_CHAIN
if (active_exception != MP_OBJ_NULL) {
mp_store_attr(obj, MP_QSTR___context__, active_exception);
}
#endif
// push exception object so it can be handled by bytecode
PUSH(MP_OBJ_FROM_PTR(nlr.ret_val));
PUSH(obj);
code_state->sp = sp;

#if MICROPY_STACKLESS
Expand Down
Loading