Parsed its arguments correctly but had an empty body (a literal
"// TODO: Implement DATEDIFF." comment), never setting a result - isNumber
was left uninitialized on this path.
Matches MySQL's own DATEDIFF(expr1, expr2): expr1 - expr2 expressed as a
whole number of days, ignoring time-of-day. Dates parse as YYYY-MM-DD via
strptime (a longer "YYYY-MM-DD HH:MM:SS" value parses fine too, since
strptime only consumes what the format string asks for) and are
interpreted as UTC via timegm() rather than mktime(), so the result
doesn't shift with the server's own timezone. An unparseable date throws
a clear, specific error instead of producing garbage.
Verified via real jet-2.0 runs: datediff('2026-08-26','2026-08-20') -> 6,
the reverse -> -6, same date -> 0, a full non-leap year -> 365, and an
invalid date string throws cleanly rather than crashing or silently
returning nonsense. Full 9-script regression suite still passes.
Both matched their function name but had empty branch bodies, silently
producing nothing regardless of arguments. reverse(str) follows the same
single-argument shape as trim/toupper/tolower; concat(a, b, ...) follows
the same variadic comma-separated shape as max/min.
reverse() converts to std::string and reverses in place via the already-
imported <algorithm> (std::reverse), then assigns back through MString's
std::string constructor. concat() starts from the first argument and
appends each subsequent one via MString's own operator<<(ZString).
Verified via real jet-2.0 runs: reverse('hello') -> "olleh",
concat('a','b','c') -> "abc", concat('x','y') -> "xy". Full 9-script
regression suite (custom tag scoping, dump tag tree, try/catch, if/else,
keyword sigil) still passes unchanged.
Found while writing a new Expressions Reference chapter for the manual,
where every function got run for real before being documented rather
than trusted from reading the source alone - substring('helloworld', 7)
threw "operand is not valid." immediately after producing the correct
answer, confirmed via debug tracing showing an extra Operand constructed
on empty input right after the 2-arg case finished.
Root cause: a missing pair of braces around the 3-argument-only branch
meant Operand parm3(in, tag) and its own closing-paren check ran
unconditionally, even after the 2-arg form's own "if(in.ifNext(')'))"
check had already matched and produced string = parm1.string.substring(
parm2.string.asInteger()) - so it immediately tried to parse a third
argument from whatever was left (nothing, since the real closing paren
was already consumed), and construction on empty input always throws.
The 3-argument form (substring(str, start, length)) was unaffected,
since its own path never hit the first if-branch's early exit.
Fixed by properly scoping the 3-arg branch inside an else, so it only
runs when the 2-arg form's own closing paren wasn't found. Verified both
forms with real jet-2.0 runs: substring('helloworld', 7) -> "rld",
substring('helloworld', 0, 5) -> "hello".
When resolving a bare $[var]/#[var] reference, Operand set .boolean =
true whenever the value's text was EITHER "true" OR "false" -
effectively "boolean = looks-boolean-ish" rather than reflecting which
of the two it actually was. <if expr="..">/<while expr="..">check
exactly this .boolean field (see __if.cpp/__while.cpp), so <if
expr="$[flag]"> took the true branch even when flag held the literal
text "false".
Fixed to boolean = (string == "true").
Verified live: a variable set to value="false" now correctly takes an
<if expr="$[var]">'s false branch, and value="true" still correctly
takes the true branch. Re-ran tests/posttest.sh - unaffected, no
regression.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>