Skip to content

Commit 8158c2b

Browse files
thilinarmtbclaude
andcommitted
Include Python traceback in libnomp error logs
Python C-API failures in the nomp_py_* functions previously reported only a generic message (e.g. "Calling realize_reduction() function failed."), discarding the actual Python exception. This made debugging issues like the loopy reduction failure much harder than necessary. Add nomp_py_err_str() to capture the active Python exception and format it (with traceback) via traceback.format_exception, and route the shared check_py_call/check_py_str/check_error_ macros through a new nomp_py_log_() that appends the traceback to the nomp log. All nomp_py_* functions gain the traceback at once since they share these macros. Add test_python_traceback_in_log (nomp-api-100) asserting the traceback, including the raised NameError, appears in the error string. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f94c55c commit 8158c2b

2 files changed

Lines changed: 109 additions & 3 deletions

File tree

src/loopy.c

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,92 @@ static char backend[NOMP_MAX_BUFFER_SIZE + 1];
66
static PyObject *py_backend_str = NULL;
77
static PyObject *py_pymbolic_to_symengine_str = NULL;
88

9+
/**
10+
* Fetch the currently set Python exception (if any) and format it, including
11+
* the traceback, into a heap allocated C-string. Returns NULL when no Python
12+
* exception is set. The caller is responsible for freeing the returned string
13+
* with nomp_free(). This consumes (clears) the Python error indicator.
14+
*/
15+
static char *nomp_py_err_str(void) {
16+
if (!PyErr_Occurred()) return NULL;
17+
18+
PyObject *type, *value, *traceback;
19+
PyErr_Fetch(&type, &value, &traceback);
20+
PyErr_NormalizeException(&type, &value, &traceback);
21+
22+
char *result = NULL;
23+
24+
// Prefer traceback.format_exception() for a full, Python-like traceback.
25+
PyObject *py_tb_module = PyImport_ImportModule("traceback");
26+
if (py_tb_module) {
27+
PyObject *py_format =
28+
PyObject_GetAttrString(py_tb_module, "format_exception");
29+
if (py_format) {
30+
PyObject *py_lines = PyObject_CallFunctionObjArgs(
31+
py_format, type ? type : Py_None, value ? value : Py_None,
32+
traceback ? traceback : Py_None, NULL);
33+
if (py_lines) {
34+
PyObject *py_sep = PyUnicode_FromString("");
35+
PyObject *py_joined = PyUnicode_Join(py_sep, py_lines);
36+
if (py_joined) {
37+
const char *str = PyUnicode_AsUTF8(py_joined);
38+
if (str) result = strndup(str, BUFSIZ);
39+
Py_DECREF(py_joined);
40+
}
41+
Py_XDECREF(py_sep), Py_DECREF(py_lines);
42+
}
43+
Py_DECREF(py_format);
44+
}
45+
Py_DECREF(py_tb_module);
46+
}
47+
48+
// Fall back to str(value) if traceback formatting was unavailable.
49+
if (!result && value) {
50+
PyObject *py_str = PyObject_Str(value);
51+
if (py_str) {
52+
const char *str = PyUnicode_AsUTF8(py_str);
53+
if (str) result = strndup(str, BUFSIZ);
54+
Py_DECREF(py_str);
55+
}
56+
}
57+
58+
// Clear any error raised while formatting the exception above.
59+
PyErr_Clear();
60+
Py_XDECREF(type), Py_XDECREF(value), Py_XDECREF(traceback);
61+
62+
return result;
63+
}
64+
65+
/**
66+
* Log a libnomp error, appending the active Python exception and its traceback
67+
* (if any) to the message. \p file and \p line are forwarded from the call site
68+
* so the recorded log points at the failing C call, not this helper. Used via
69+
* the check_*() macros below; returns the log id from nomp_log_().
70+
*/
71+
static int nomp_py_log_(int errorno, const char *file, unsigned line,
72+
const char *fmt, ...) {
73+
char buf[BUFSIZ];
74+
va_list args;
75+
va_start(args, fmt);
76+
vsnprintf(buf, BUFSIZ, fmt, args);
77+
va_end(args);
78+
79+
char *py_err = nomp_py_err_str();
80+
if (py_err) {
81+
char full[BUFSIZ];
82+
snprintf(full, BUFSIZ, "%s Python traceback:\n%s", buf, py_err);
83+
nomp_free(&py_err);
84+
// Pass the composed message through a "%s" format so any '%' characters
85+
// in the Python traceback are not interpreted as conversions.
86+
return nomp_log_("%s", errorno, NOMP_ERROR, file, line, full);
87+
}
88+
89+
return nomp_log_("%s", errorno, NOMP_ERROR, file, line, buf);
90+
}
91+
992
#define check_error_(obj, err, ...) \
1093
{ \
11-
if (!obj) \
12-
return nomp_log(err, NOMP_ERROR, \
13-
NOMP_FIRST(__VA_ARGS__) NOMP_REST(__VA_ARGS__)); \
94+
if (!(obj)) return nomp_py_log_(err, __FILE__, __LINE__, __VA_ARGS__); \
1495
}
1596

1697
#define check_py_str(obj, str) \

tests/nomp-api-100.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,30 @@ static int test_syntax_error_in_transform_function(void) {
124124
return 0;
125125
}
126126

127+
// When a transform function raises a Python exception, the nomp error log
128+
// should include the Python traceback (e.g. the raised NameError).
129+
static int test_python_traceback_in_log(void) {
130+
const char *clauses[4] = {"transform", "nomp_api_100",
131+
"function_with_syntax_error", 0};
132+
133+
static int id = -1;
134+
int err = nomp_jit(&id, valid_knl, clauses, 2, "a", sizeof(int), NOMP_PTR,
135+
"N", sizeof(int), NOMP_INT);
136+
nomp_test_assert(nomp_get_err_no(err) == NOMP_PY_CALL_FAILURE);
137+
138+
char *log = nomp_get_err_str(err);
139+
// The nomp message is followed by the captured Python traceback. The
140+
// offending line `return kn` raises a NameError, which must appear in
141+
// the log along with the traceback header.
142+
int eq = logcmp(log, "Python traceback:") &&
143+
logcmp(log, "Traceback (most recent call last):") &&
144+
logcmp(log, "NameError");
145+
nomp_free(&log);
146+
nomp_test_assert(eq);
147+
148+
return 0;
149+
}
150+
127151
int main(int argc, const char *argv[]) {
128152
nomp_test_check(nomp_init(argc, argv));
129153

@@ -135,6 +159,7 @@ int main(int argc, const char *argv[]) {
135159
err |= SUBTEST(test_empty_user_callback);
136160
err |= SUBTEST(test_syntax_error_in_kernel);
137161
err |= SUBTEST(test_syntax_error_in_transform_function);
162+
err |= SUBTEST(test_python_traceback_in_log);
138163

139164
nomp_test_check(nomp_finalize());
140165

0 commit comments

Comments
 (0)