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: