From 76ea89e79cb539b2bbf5e07bd5dc67af420226a4 Mon Sep 17 00:00:00 2001 From: Brad Arant Date: Wed, 26 Aug 2026 09:00:18 -0700 Subject: [PATCH] Add / tag, fix a real infinite-loop bug it exposed in Tag's split-container mechanism ...container......container2...'s own convention> intercepts any error that would otherwise propagate up and halt the whole script - a coreutils::Exception (what every other tag actually throws), any other std::exception (e.g. the std::filesystem::filesystem_error can raise), or anything else. Optional name=/scope= keywords bind the caught error's message to a variable for the catch block to reference, matching 's own name=/scope= convention. New class named __trytag rather than __try: transitively defines a preprocessor macro "#define __try try" (an SEH- compatibility shim pulled in on this toolchain) that silently rewrites the identifier __try to the keyword try wherever it appears, breaking compilation - confirmed via `g++ -E -dM` before picking this name. The JET tag name itself is still plain . Reuses the same container/container2 split mechanism / already has (Tag's constructor takes an optional splitTagName), which surfaced a real, previously-latent bug: the code that consumes the split tag's own opening delimiter after a match was hardcoded to the literal string "" instead of being built from splitTagName. This happened to work for every existing split-tag user (, , all use "else"), but for 's "catch" the hardcoded ifNext("") never matched, so the cursor never advanced past and the outer scan loop matched the same unconsumed forever - a genuine infinite loop, not just wrong output, confirmed via a real hung jet-2.0 run before diagnosis. Fixed by building the actual expected delimiter ("<" + splitTagName + ">") dynamically instead. Verified via real jet-2.0 script execution (not just compilation): a caught failure correctly binds its error text to a name= variable and skips straight to the catch block, discarding whatever partial output the failed attempt had already written; an untriggered runs its own content normally and skips entirely; a with no at all silently swallows the error and the script continues past it. Also regression-tested / and (both depend on the exact same fixed code path) to confirm nothing broke for the existing "else" case. --- Tag.cpp | 22 ++++++++++++++++++++-- __trytag.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ __trytag.h | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 __trytag.cpp create mode 100644 __trytag.h diff --git a/Tag.cpp b/Tag.cpp index 45d0be0..2d5059b 100644 --- a/Tag.cpp +++ b/Tag.cpp @@ -25,6 +25,7 @@ #include "__dotag.h" #include "__stream.h" #include "__dump.h" +#include "__trytag.h" #include "Operand.h" #include @@ -90,8 +91,22 @@ namespace jet { } else if(hasSplitTag && ifSplitTagName(in)) { hasContainer2 = true; container = coreutils::ZString(start, end - start); - in.ifNext(""); - start = in.getCursor(); + // ifSplitTagName() is a pure lookahead (push()/pop()'s the + // cursor back regardless of match - see its own body), so + // the actual split-tag opening delimiter still has to be + // consumed here. This used to be hardcoded to the literal + // "" - fine for // (which all use + // splitTagName "else"), but wrong for any other split tag + // name (e.g. 's "catch") - a hardcoded literal that + // never matches means this ifNext() silently fails, the + // cursor never advances past , and the outer loop + // matches the same unconsumed on every subsequent + // iteration forever. Build the real expected delimiter + // from splitTagName itself so it works for any split tag. + coreutils::MString splitOpenTag; + splitOpenTag << "<" << splitTagName << ">"; + in.ifNext(splitOpenTag); + start = in.getCursor(); } else if(ifNested(in)) { } else in.nextChar(); @@ -211,6 +226,9 @@ namespace jet { } else if(ifTagName(in, "until")) { __until _until(in, out, global, this, local); continue; + } else if(ifTagName(in, "try")) { + __trytag _trytag(in, out, global, this, local); + continue; } else if(ifTagName(in, "header")) { __header _header(in, out, global, this, local); continue; diff --git a/__trytag.cpp b/__trytag.cpp new file mode 100644 index 0000000..12ef1a5 --- /dev/null +++ b/__trytag.cpp @@ -0,0 +1,41 @@ +#include "__trytag.h" +#include "Exception.h" +#include + +namespace jet { + + __trytag::__trytag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) + : Tag(in, parentOut, global, parent, this, "catch") { + + coreutils::MString errorText; + bool failed = false; + + try { + processContainer(container); + } catch(coreutils::Exception &e) { + errorText = e.text; + failed = true; + } catch(std::exception &e) { + errorText = e.what(); + failed = true; + } catch(...) { + errorText = "unknown error"; + failed = true; + } + + if(failed) { + // Discard whatever the failed attempt had already written before + // it threw - out is this tag's own accumulating output buffer + // (processContainer(container) above wrote into it directly), so + // without this a caught mid-render failure would leave partial + // try-block output mixed in ahead of the catch block's own. + out = ""; + if(keywordDefined("name")) + storeVariable(resolveKeyword("name"), errorText, + keywordDefined("scope") ? resolveKeyword("scope") : "global"); + if(hasContainer2) + processContainer(container2); + } + } + +} diff --git a/__trytag.h b/__trytag.h new file mode 100644 index 0000000..f06ab3d --- /dev/null +++ b/__trytag.h @@ -0,0 +1,46 @@ +#ifndef ____trytag_h__ +#define ____trytag_h__ + +#include "Tag.h" +#include "ZString.h" +#include "MString.h" + +namespace jet { + + /// + /// ...container......container2... + /// + /// Runs its own container the same as any other tag; if that raises an + /// error that would otherwise propagate up and halt the whole script - + /// a coreutils::Exception (what every other tag actually throws), any + /// other std::exception (e.g. the std::filesystem::filesystem_error + /// can raise), or anything else - the container (if + /// present) runs instead, with any partial output the failed attempt + /// had already written discarded first so the result is cleanly either + /// the try's output or the catch's, never a mix of both. + /// + /// Optional keywords, both meaningful only on catch: name (a variable + /// to receive the caught error's message text) and scope (global by + /// default, or local/parent - same convention as 's own name/scope + /// pair). + /// + /// Named __trytag rather than __try: transitively defines a + /// preprocessor macro "#define __try try" (an SEH-compatibility shim + /// pulled in on this toolchain) that silently rewrites the identifier + /// __try to the literal keyword try wherever it appears, breaking + /// compilation - confirmed via `g++ -E -dM` before picking this name, + /// not guessed. The JET tag name itself is still plain - this + /// only affects the internal C++ class name, matched by ifTagName(in, + /// "try") in Tag.cpp regardless of what the class is called. + /// + + class __trytag : public Tag { + + public: + __trytag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local); + + }; + +} + +#endif