Add <try>/<catch> tag, fix a real infinite-loop bug it exposed in Tag's

split-container mechanism

<try>...container...<catch>...container2...</catch-less, matching <else>'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 <whiledir> 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 <for>'s own
name=/scope= convention. New class named __trytag rather than __try: <ios>
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 <try>.

Reuses the same container/container2 split mechanism <if>/<else> 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
"<else>" instead of being built from splitTagName. This happened to work
for every existing split-tag user (<if>, <while>, <until> all use "else"),
but for <try>'s "catch" the hardcoded ifNext("<else>") never matched, so
the cursor never advanced past <catch> and the outer scan loop matched the
same unconsumed <catch> 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 <read> 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 <try> runs its own
content normally and skips <catch> entirely; a <try> with no <catch> at
all silently swallows the error and the script continues past it. Also
regression-tested <if>/<else> and <while> (both depend on the exact same
fixed code path) to confirm nothing broke for the existing "else" case.
This commit is contained in:
Brad Arant committed 2026-08-26 09:00:18 -07:00
1 parent bc3c50f045
commit 76ea89e79c
3 files changed
+107 -2

No files matched your search

+20 -2
View File
@@ -25,6 +25,7 @@
#include "__dotag.h"
#include "__stream.h"
#include "__dump.h"
#include "__trytag.h"
#include "Operand.h"
#include <iostream>
@@ -90,8 +91,22 @@ namespace jet {
} else if(hasSplitTag && ifSplitTagName(in)) {
hasContainer2 = true;
container = coreutils::ZString(start, end - start);
in.ifNext("<else>");
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
// "<else>" - fine for <if>/<while>/<until> (which all use
// splitTagName "else"), but wrong for any other split tag
// name (e.g. <try>'s "catch") - a hardcoded literal that
// never matches means this ifNext() silently fails, the
// cursor never advances past <catch>, and the outer loop
// matches the same unconsumed <catch> 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;
+41
View File
@@ -0,0 +1,41 @@
#include "__trytag.h"
#include "Exception.h"
#include <exception>
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);
}
}
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef ____trytag_h__
#define ____trytag_h__
#include "Tag.h"
#include "ZString.h"
#include "MString.h"
namespace jet {
///
/// <try>...container...<catch>...container2...</catch></try>
///
/// 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
/// <whiledir> can raise), or anything else - the <catch> 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 <for>'s own name/scope
/// pair).
///
/// Named __trytag rather than __try: <ios> 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 <try> - 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