From 5c29da52dc6f74b513982fa6f2648c2e87f06af2 Mon Sep 17 00:00:00 2001 From: Andy Kopra Date: Fri, 25 Sep 2026 22:12:48 +0200 Subject: [PATCH] An error inside a klammer is reported at your document; the image search path Assembled from development commit 632297b7657a. Since 1b00738: when something goes wrong while a klammer is being applied, the report begins with the line of your document that applied it, then lists the klammers being applied and where inside the klammer set the fault was raised; the Python binding's line and column are the application's too. The default image search path is the document's directory, imgsrc beside it and ../imgsrc, and a not-found report shows exactly those. --- README.md | 2 +- kpy/klammertext.cpp | 6 ++-- mac/error.cpp | 67 +++++++++++++++++++++++++++++++++++----- mac/error.h | 15 +++++++++ mac/machine.cpp | 12 ++++++- sks/image/image.k | 4 +-- sks/image/image_cache.py | 5 ++- tst/eval_test.sh | 25 +++++++++++++++ tst/kpy_test.sh | 14 +++++++++ 9 files changed, 135 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 18e8f87..7832a26 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ are regenerated on each release — patches cannot be merged directly. Report problems (or send patches) to the author; accepted changes are applied to the development tree and appear in a following snapshot. -This snapshot was assembled from development commit `8ed24ef03b36`. +This snapshot was assembled from development commit `632297b7657a`. ## License diff --git a/kpy/klammertext.cpp b/kpy/klammertext.cpp index a4f4a3b..8f44bd3 100644 --- a/kpy/klammertext.cpp +++ b/kpy/klammertext.cpp @@ -110,10 +110,10 @@ PyObject* raise_error(const Error& e) set_attr(inst, "kind", PyUnicode_FromString(e.m_type.c_str())); set_attr(inst, "description", PyUnicode_FromString(justify(e.m_desc, 80, 0).c_str())); set_attr(inst, "filename", - e.m_loc.m_filename.empty() ? nullptr : PyUnicode_FromString(e.m_loc.m_filename.c_str())); - set_attr(inst, "line", e.m_loc.m_line > -1 ? PyLong_FromLong(e.m_loc.m_line) : nullptr); + e.location().m_filename.empty() ? nullptr : PyUnicode_FromString(e.location().m_filename.c_str())); + set_attr(inst, "line", e.location().m_line > -1 ? PyLong_FromLong(e.location().m_line) : nullptr); // 1-based, as the report prints it (the Locator holds a 0-based index). - set_attr(inst, "column", e.m_loc.m_chr > -1 ? PyLong_FromLong(e.m_loc.m_chr + 1) : nullptr); + set_attr(inst, "column", e.location().m_chr > -1 ? PyLong_FromLong(e.location().m_chr + 1) : nullptr); if (PyObject* cause = Eval_python::take_last_exception()) { PyException_SetCause(inst, cause); // steals the reference } diff --git a/mac/error.cpp b/mac/error.cpp index f666b16..2e32c0b 100644 --- a/mac/error.cpp +++ b/mac/error.cpp @@ -15,6 +15,23 @@ bool display_source(const std::string& filename_arg) } } +// "file, line N, character M" for a location, under the same rule as the +// headline: the file only when display_source() says so. +static std::string where(const Locator& loc) +{ + std::stringstream ss {}; + if (display_source(loc.m_filename)) { + ss << loc.m_filename; + } + if (loc.m_line > -1) { + ss << (ss.str().empty() ? "line " : ", line ") << loc.m_line; + } + if (loc.m_chr > -1) { + ss << ", character " << loc.m_chr + 1; + } + return ss.str(); +} + std::string Error::message(const std::string& epilog) const { // The separator is not cosmetic: the description ends with a sentence, and @@ -28,16 +45,52 @@ std::string Error::message(const std::string& epilog) const } std::stringstream ss {}; ss << command_name << " (" << m_type << " error)"; - if (display_source(m_loc.m_filename)) { - ss << ":\n " << m_loc.m_filename; + // The headline is location(): the outermost application when the error + // escaped through klammer applications, which is the user's document. + const Locator& head = location(); + if (display_source(head.m_filename)) { + ss << ":\n " << head.m_filename; } - if (m_loc.m_line > -1) { - ss << ", line " << m_loc.m_line; + if (head.m_line > -1) { + ss << ", line " << head.m_line; } - if (m_loc.m_chr > -1) { - ss << ", character " << m_loc.m_chr + 1; + if (head.m_chr > -1) { + ss << ", character " << head.m_chr + 1; } - ss << ":\n\n" << justify(desc, 80, 0); + ss << ":\n\n"; + // Then the chain, outermost first, each level saying which klammer was + // applied and, below the top, where; last, where the error itself was + // raised when that is not the innermost application. Lines begin with + // two spaces so justify() keeps them verbatim. A recursion error + // carries hundreds of levels: the outermost, an ellipsis and the two + // innermost say what a person needs. + if (!m_applications.empty()) { + const std::size_t n = m_applications.size(); + auto level = [&](std::size_t i, bool with_place) { // i: 0 = innermost + ss << " applying @" << m_applications[i].first; + if (with_place) { + ss << " at " << where(m_applications[i].second); + } + ss << "\n"; + }; + level(n - 1, false); + if (n > 4) { + ss << " ... " << n - 3 << " applications ...\n"; + level(1, true); + level(0, true); + } else { + for (std::size_t i = n - 1; i-- > 0; ) { + level(i, true); + } + } + const Locator& inner = m_applications.front().second; + if (m_loc.m_filename != inner.m_filename || m_loc.m_line != inner.m_line + || m_loc.m_chr != inner.m_chr) { + ss << " at " << where(m_loc) << "\n"; + } + ss << "\n"; + } + ss << justify(desc, 80, 0); return ss.str(); } diff --git a/mac/error.h b/mac/error.h index ba4a307..451e7c6 100644 --- a/mac/error.h +++ b/mac/error.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include "locator.h" @@ -27,9 +29,22 @@ public: std::string message(const std::string& epilog="") const; void print_message(const std::string& epilog=""); + // Where the error is reported: the OUTERMOST klammer application it + // escaped through when there is one -- the place in the user's document + // -- else where it was raised. The commands' report headlines this and + // the Python binding's line/column are this (2026-09-25; before, an + // error raised inside a klammer's body, such as an @eval in + // sks/image/image.k, was located there and the document never named). + const Locator& location() const + { return m_applications.empty() ? m_loc : m_applications.back().second; } + std::string m_type {}; std::string m_desc {}; Locator m_loc; // {}; + // The klammer applications the error escaped through, innermost first: + // (name, where applied). Filled by Machine::apply_klammer_registry as + // the exception unwinds; message() prints the chain after the headline. + std::vector> m_applications {}; }; diff --git a/mac/machine.cpp b/mac/machine.cpp index a139c44..40305d9 100644 --- a/mac/machine.cpp +++ b/mac/machine.cpp @@ -857,7 +857,17 @@ void Machine::apply_klammer_registry( { (void)K::log(3, "Klammer"); std::string name = trim_char(begin->m_text, '@'); - katom_list applied_katoms = apply_klammer(klammer_registry.m_klammers[name], target, begin, end); + katom_list applied_katoms {}; + try { + applied_katoms = apply_klammer(klammer_registry.m_klammers[name], target, begin, end); + } catch (Error& e) { + // The error escapes through this application: record it, so that the + // report can headline the user's document and show the chain, then + // let it go on unwinding (the exception object itself is what the + // outer handlers and the commands see). + e.m_applications.emplace_back(name, begin->m_loc); + throw; + } for (auto& k : applied_katoms) { if (k.m_type == katom_t::bar || k.m_type == katom_t::double_bar || k.m_type == katom_t::option_name) { k.m_type = katom_t::text; diff --git a/sks/image/image.k b/sks/image/image.k index 7a0c886..df77b50 100644 --- a/sks/image/image.k +++ b/sks/image/image.k @@ -4,8 +4,8 @@ @@@ ]# -@@@state Image_search_path :desc Series of directories for image search - :value . imgsrc ../imgsrc ../dbg +@@@state Image_search_path :desc Directories searched for an image's file, relative to the document + :value . imgsrc ../imgsrc @@@ @@@state Image_output_dir :desc Output directory for images :value img @@@ diff --git a/sks/image/image_cache.py b/sks/image/image_cache.py index 8a81e84..ee0280e 100644 --- a/sks/image/image_cache.py +++ b/sks/image/image_cache.py @@ -98,7 +98,10 @@ class Image_cache: "latex" : ["png", "jpg", "jpeg", "JPEG", "pdf", "eps"] } if not os.path.exists(self.dir): os.makedirs(self.dir) - self.search = self.search + [f"{os.path.dirname(cache_dir)}/imgsrc"] + # The search path is the Image_search_path state variable and nothing + # else (until 2026-09-25 an imgsrc beside the CACHE directory was + # appended -- /dev/imgsrc when the cache is in /dev/shm -- and every + # not-found report named it). def construct_cache_filename(self, target, filename): basename, ext = os.path.splitext(filename) diff --git a/tst/eval_test.sh b/tst/eval_test.sh index 6ab5490..7b8f6a5 100755 --- a/tst/eval_test.sh +++ b/tst/eval_test.sh @@ -248,6 +248,31 @@ else fail "20. exit $hstatus" "$(plain < "$ERRF" | head -2)" fi +# An error raised INSIDE a klammer's body (here an @eval that divides by +# zero) is reported at the application in the user's document -- the +# headline -- with the chain of applications and the raising place below +# (2026-09-25; before, the headline was the @eval's own line, which for an +# SKS klammer is a line of a .k file the user never wrote). Wrap-insensitive +# greps: the report is justified to 80 columns. +CTX=$(mktemp -d "${TMPDIR:-/tmp}/eval_ctx.XXXXXX") +printf '@@bad x : @eval 1/*x* @ @@\n@@outer y : @bad *y* @ @@\ntext\n@bad 0 @\n' > "$CTX/ctx.kt" +printf '@@bad x : @eval 1/*x* @ @@\n@@outer y : @bad *y* @ @@\ntext\n@outer 0 @\n' > "$CTX/nested.kt" +"$K/bin/ktext" "$CTX/ctx.kt" --klammersets none -d >"$OUTF" 2>"$ERRF"; cstatus=$? +ctext=$(tr '\n' ' ' < "$ERRF" | tr -s ' ') +if [ $cstatus -ne 0 ] && case "$ctext" in *"ctx.kt, line 4, character 1:"*"applying @bad"*"at "*"ctx.kt, line 1, character "*"ZeroDivisionError"*) true;; *) false;; esac; then + pass "21. an error inside a klammer body is headlined at the application, with the chain" +else + fail "21. exit $cstatus" "$(plain < "$ERRF" | head -6 | tr '\n' '|')" +fi +"$K/bin/ktext" "$CTX/nested.kt" --klammersets none -d >"$OUTF" 2>"$ERRF"; cstatus=$? +ctext=$(tr '\n' ' ' < "$ERRF" | tr -s ' ') +if [ $cstatus -ne 0 ] && case "$ctext" in *"nested.kt, line 4, character 1:"*"applying @outer"*"applying @bad at "*"nested.kt, line 2, character "*"at "*"nested.kt, line 1, character "*) true;; *) false;; esac; then + pass "22. ... nested applications are listed outermost first, the raising place last" +else + fail "22. exit $cstatus" "$(plain < "$ERRF" | head -7 | tr '\n' '|')" +fi +rm -rf "$CTX" + echo echo "===========" echo "Results: ${PASS} passed, ${FAIL} failed" diff --git a/tst/kpy_test.sh b/tst/kpy_test.sh index 47d29e5..60fb5a2 100755 --- a/tst/kpy_test.sh +++ b/tst/kpy_test.sh @@ -262,6 +262,20 @@ except kt.Error as e: EOF expect "12. an undefined klammer: Definition_error with kind, line, column, report" 'error Definition_error definition 1 1 True True' +# An error raised inside a klammer's body is located, for a script, at the +# APPLICATION (line 3 here), not at the @eval in the definition (line 1). +run <<'EOF' +import klammertext as kt +M = kt.Machine(klammersets="none") +try: + M.read("@@bad x : @eval 1/*x* @ @@\ntext\n@bad 0 @") + M.apply() + print("no error") +except kt.Error as e: + print("error", e.line, e.column, "applying @bad" in str(e)) +EOF +expect "12a. an error inside a klammer body is located at the application" 'error 3 1 True' + run <<'EOF' import klammertext as kt try: