diff --git a/src/loopy.c b/src/loopy.c index 0efe073b..c3eb1663 100644 --- a/src/loopy.c +++ b/src/loopy.c @@ -6,11 +6,92 @@ static char backend[NOMP_MAX_BUFFER_SIZE + 1]; static PyObject *py_backend_str = NULL; static PyObject *py_pymbolic_to_symengine_str = NULL; +/** + * Fetch the currently set Python exception (if any) and format it, including + * the traceback, into a heap allocated C-string. Returns NULL when no Python + * exception is set. The caller is responsible for freeing the returned string + * with nomp_free(). This consumes (clears) the Python error indicator. + */ +static char *nomp_py_err_str(void) { + if (!PyErr_Occurred()) return NULL; + + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + + char *result = NULL; + + // Prefer traceback.format_exception() for a full, Python-like traceback. + PyObject *py_tb_module = PyImport_ImportModule("traceback"); + if (py_tb_module) { + PyObject *py_format = + PyObject_GetAttrString(py_tb_module, "format_exception"); + if (py_format) { + PyObject *py_lines = PyObject_CallFunctionObjArgs( + py_format, type ? type : Py_None, value ? value : Py_None, + traceback ? traceback : Py_None, NULL); + if (py_lines) { + PyObject *py_sep = PyUnicode_FromString(""); + PyObject *py_joined = PyUnicode_Join(py_sep, py_lines); + if (py_joined) { + const char *str = PyUnicode_AsUTF8(py_joined); + if (str) result = strndup(str, BUFSIZ); + Py_DECREF(py_joined); + } + Py_XDECREF(py_sep), Py_DECREF(py_lines); + } + Py_DECREF(py_format); + } + Py_DECREF(py_tb_module); + } + + // Fall back to str(value) if traceback formatting was unavailable. + if (!result && value) { + PyObject *py_str = PyObject_Str(value); + if (py_str) { + const char *str = PyUnicode_AsUTF8(py_str); + if (str) result = strndup(str, BUFSIZ); + Py_DECREF(py_str); + } + } + + // Clear any error raised while formatting the exception above. + PyErr_Clear(); + Py_XDECREF(type), Py_XDECREF(value), Py_XDECREF(traceback); + + return result; +} + +/** + * Log a libnomp error, appending the active Python exception and its traceback + * (if any) to the message. \p file and \p line are forwarded from the call site + * so the recorded log points at the failing C call, not this helper. Used via + * the check_*() macros below; returns the log id from nomp_log_(). + */ +static int nomp_py_log_(int errorno, const char *file, unsigned line, + const char *fmt, ...) { + char buf[BUFSIZ]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, BUFSIZ, fmt, args); + va_end(args); + + char *py_err = nomp_py_err_str(); + if (py_err) { + char full[BUFSIZ]; + snprintf(full, BUFSIZ, "%s Python traceback:\n%s", buf, py_err); + nomp_free(&py_err); + // Pass the composed message through a "%s" format so any '%' characters + // in the Python traceback are not interpreted as conversions. + return nomp_log_("%s", errorno, NOMP_ERROR, file, line, full); + } + + return nomp_log_("%s", errorno, NOMP_ERROR, file, line, buf); +} + #define check_error_(obj, err, ...) \ { \ - if (!obj) \ - return nomp_log(err, NOMP_ERROR, \ - NOMP_FIRST(__VA_ARGS__) NOMP_REST(__VA_ARGS__)); \ + if (!(obj)) return nomp_py_log_(err, __FILE__, __LINE__, __VA_ARGS__); \ } #define check_py_str(obj, str) \ diff --git a/tests/nomp-api-100.c b/tests/nomp-api-100.c index 14133bb8..d1b218b4 100644 --- a/tests/nomp-api-100.c +++ b/tests/nomp-api-100.c @@ -124,6 +124,30 @@ static int test_syntax_error_in_transform_function(void) { return 0; } +// When a transform function raises a Python exception, the nomp error log +// should include the Python traceback (e.g. the raised NameError). +static int test_python_traceback_in_log(void) { + const char *clauses[4] = {"transform", "nomp_api_100", + "function_with_syntax_error", 0}; + + static int id = -1; + int err = nomp_jit(&id, valid_knl, clauses, 2, "a", sizeof(int), NOMP_PTR, + "N", sizeof(int), NOMP_INT); + nomp_test_assert(nomp_get_err_no(err) == NOMP_PY_CALL_FAILURE); + + char *log = nomp_get_err_str(err); + // The nomp message is followed by the captured Python traceback. The + // offending line `return kn` raises a NameError, which must appear in + // the log along with the traceback header. + int eq = logcmp(log, "Python traceback:") && + logcmp(log, "Traceback (most recent call last):") && + logcmp(log, "NameError"); + nomp_free(&log); + nomp_test_assert(eq); + + return 0; +} + int main(int argc, const char *argv[]) { nomp_test_check(nomp_init(argc, argv)); @@ -135,6 +159,7 @@ int main(int argc, const char *argv[]) { err |= SUBTEST(test_empty_user_callback); err |= SUBTEST(test_syntax_error_in_kernel); err |= SUBTEST(test_syntax_error_in_transform_function); + err |= SUBTEST(test_python_traceback_in_log); nomp_test_check(nomp_finalize());