diff --git a/lexilla/np3_patches/003_LexMarkdown_AtTermStart.md b/lexilla/np3_patches/003_LexMarkdown_AtTermStart.md new file mode 100644 index 0000000000..f2c19c5546 --- /dev/null +++ b/lexilla/np3_patches/003_LexMarkdown_AtTermStart.md @@ -0,0 +1,106 @@ +# Patch 003: LexMarkdown — AtTermStart accepts opening punctuation + +**File:** `lexilla/lexers/LexMarkdown.cxx` +**Status:** NP3 local fix (candidate for upstream submission) +**First applied:** 2026-05-21 + +## Problem + +The stock Lexilla Markdown lexer fails to recognise inline spans that +open immediately after common opening punctuation. The user-reported +case is inline code: + +``` +(`Hello`) <- not highlighted +( `Hello`) <- highlighted (extra space) +``` + +The same defect rejects: + +``` +[`x`] {`x`} <`x`> "`x`" '`x`' +(**x**) [**x**] (~~x~~) (*x*) (_x_) +``` + +VS Code and other CommonMark-compliant renderers accept all of these. +Per CommonMark: + +- **Code spans** have no left-flank restriction whatsoever. +- **Emphasis / strong** opening uses left-flanking-delimiter-run rules; + a delimiter preceded by Unicode punctuation and followed by a + non-whitespace non-punctuation char is a valid left flank. + +## Root cause + +`AtTermStart` (helper used by `IsCompleteStyleRegion` and the multi- +backtick code-span entry point) only returns `true` when `chPrev` is +whitespace or start-of-file: + +```cpp +bool AtTermStart(const StyleContext &sc) noexcept { + return sc.currentPos == 0 || sc.chPrev == 0 || isspacechar(sc.chPrev); +} +``` + +With `chPrev = '('`, the guard rejects the opening backtick and the +span never enters `SCE_MARKDOWN_CODE`/`SCE_MARKDOWN_CODE2`. Same for +`SCE_MARKDOWN_STRONG{1,2}`, `SCE_MARKDOWN_EM{1,2}`, and +`SCE_MARKDOWN_STRIKEOUT`. + +## Fix + +Extend `AtTermStart` to additionally accept `(`, `[`, `{`, `<`, `"`, +`'` as valid left-edge characters. This covers all bracketed and +quoted forms users typically write, without loosening behaviour for +mid-word delimiters (`foo*bar*` remains unchanged — `chPrev = 'o'` is +still rejected). + +```cpp +bool AtTermStart(const StyleContext &sc) noexcept { + if (sc.currentPos == 0 || sc.chPrev == 0 || isspacechar(sc.chPrev)) + return true; + switch (sc.chPrev) { + case '(': case '[': case '{': case '<': + case '"': case '\'': + return true; + default: + return false; + } +} +``` + +## Scope + +Affects all inline tokens gated by `IsCompleteStyleRegion`: + +- `` ` `` (single-backtick code) +- `` `` `` `` (multi-backtick code, via direct `AtTermStart(sc)` call) +- `**` / `__` (strong) +- `*` / `_` (emphasis) +- `~~` (strikeout) + +Block-level constructs (headers, lists, code blocks, blockquote, +hrule, links) are untouched. + +## Visual verification + +Open `test/test_files/StyleLexers/styleLexMARKDOWN/README.md` in +Notepad3 after build. The section appended for this patch demonstrates +each adjacency variant — inline code/strong/em/strikeout following +every opener should colour correctly. + +## Upstream + +This is an upstream Lexilla defect. The fix is small and self- +contained; a PR against ScintillaOrg/lexilla would be welcome. Until +then, keep this patch. + +## Upgrade procedure + +After a Lexilla upgrade: + +1. Diff `lexilla/lexers/LexMarkdown.cxx` against the upstream copy. +2. Reapply this patch (`003_LexMarkdown_AtTermStart.patch`) if the + upstream still ships the whitespace-only `AtTermStart`. +3. If upstream has integrated the fix, retire this patch and remove + its row from `README.md`. diff --git a/lexilla/np3_patches/003_LexMarkdown_AtTermStart.patch b/lexilla/np3_patches/003_LexMarkdown_AtTermStart.patch new file mode 100644 index 0000000000..5790ffb92a --- /dev/null +++ b/lexilla/np3_patches/003_LexMarkdown_AtTermStart.patch @@ -0,0 +1,27 @@ +diff --git a/lexilla/lexers/LexMarkdown.cxx b/lexilla/lexers/LexMarkdown.cxx +index 667b3b534..f58dab187 100644 +--- a/lexilla/lexers/LexMarkdown.cxx ++++ b/lexilla/lexers/LexMarkdown.cxx +@@ -119,7 +119,21 @@ bool HasPrevLineContent(StyleContext &sc) { + } + + bool AtTermStart(const StyleContext &sc) noexcept { +- return sc.currentPos == 0 || sc.chPrev == 0 || isspacechar(sc.chPrev); ++ if (sc.currentPos == 0 || sc.chPrev == 0 || isspacechar(sc.chPrev)) ++ return true; ++ // NP3 patch: also accept common opening punctuation so inline spans ++ // (code, strong, emphasis, strikeout) can open directly after them, ++ // e.g. (`x`), [`x`], {`x`}, <`x`>, "`x`", '`x`'. CommonMark and VS ++ // Code accept these — code spans have no left-flank restriction at ++ // all, and emphasis after opening punctuation is a valid ++ // left-flanking delimiter run. ++ switch (sc.chPrev) { ++ case '(': case '[': case '{': case '<': ++ case '"': case '\'': ++ return true; ++ default: ++ return false; ++ } + } + + bool IsCompleteStyleRegion(StyleContext &sc, const char *token) { diff --git a/readme/tinyexprcpp/TinyExprPP.md b/readme/tinyexprcpp/TinyExprPP.md index ecacf3d21d..02274bee2b 100644 --- a/readme/tinyexprcpp/TinyExprPP.md +++ b/readme/tinyexprcpp/TinyExprPP.md @@ -313,9 +313,9 @@ SQRT(3^2 + 4^2)=? → 5 ### Unit Conversions ``` -72 * 0.0254=? → 1.8288 (72 inches to meters) -100 / 2.54=? → 39.3701 (100 cm to inches) -(98.6 - 32) * 5/9=? → 37 (Fahrenheit to Celsius) +72 * 0.0254=? → 1.8288 (72 inches to meters) +100 / 2.54=? → 39.3700787401575 (100 cm to inches) +(98.6 - 32) * 5/9=? → 37 (Fahrenheit to Celsius) ``` ### Programming Helpers @@ -361,7 +361,7 @@ TinyExpr++ in Notepad3 automatically adapts to the **decimal separator** of the | **Function argument separator** | `,` (comma) | `;` (semicolon) | | **Number example** | `3.14` | `3,14` | | **Function call** | `SUM(1.5, 2)` | `SUM(1,5; 2)` | -| **Inline evaluation** | `1/3=?` → `0.33333333` | `1/3=?` → `0,33333333` | +| **Inline evaluation** | `1/3=?` → `0.333333333333333` | `1/3=?` → `0,333333333333333` | ### Examples by Locale @@ -383,6 +383,77 @@ IF(2,5 > 1; 10; 20)=? → 10 --- +## C API: Boolean-Aware Evaluation (developer reference) + +The C wrapper around TinyExpr++ exposes an evaluator that renders results +of boolean-looking expressions as the words `true` / `false`, so callers +don't have to invent their own classification scheme: + +```c +#include "tinyexpr_cif.h" + +const char *te_interp_str(const char *expression, te_int_t *error); +``` + +The function evaluates the expression once and returns a thread-local +internal buffer. The returned pointer remains valid until the next call +from the same thread; `*error` follows the same convention as `te_interp()` +(`0` on success, 1-based parse-error position on failure). + +| Returned string | When | +|-----------------|------| +| `"true"` / `"false"` | Expression is lexically logical **and** evaluates to exactly `1.0` / `0.0`. | +| `"nan"`, `"inf"`, `"-inf"` | Result is non-finite (e.g., `0/0`, `LN(0)`, comparison involving `NaN`). | +| Integer-like (`%.21g`) | Finite value whose fractional part is below `1e-15` and whose magnitude is below `1e21`. | +| Decimal (`%.15g`) | All other finite results. | + +The numeric formatting mirrors Notepad3's `TinyExprToStringA` exactly, so values returned by `te_interp_str()` match what the status bar / `=?` inline-replacement would render. Hex / binary output modes are UI-level concerns and remain in `TinyExprToStringA` proper. + +### When is an expression classified as logical? + +Classification requires **both** of the following to hold: + +1. **Lexical hit** at parenthesis depth 0 (one fully-enclosing outer + pair is stripped first, so `(1==1)` is treated like `1==1`): + - a relational / equality / logical operator: `==`, `=`, `!=`, `<>`, + `<=`, `>=`, `<`, `>`, `&&`, `||`, leading `!` + - the bare keywords `true` / `false` + - an outermost call to `AND`, `OR`, `NOT`, `ISERR`, `ISERROR`, + `ISNA`, `ISNAN`, `ISEVEN`, or `ISODD` (case-insensitive) +2. **Value hit**: the finite evaluation result is exactly `0.0` or `1.0`. + +Bit-shift (`<<`, `>>`) and bit-rotate (`<<<`, `>>>`) are consumed by the +scanner without triggering classification. Block comments (`/* … */`) and +line comments (`// …`) are skipped, mirroring the parser. + +`IF` / `IFS` are intentionally **not** in the predicate list — they return +arbitrary user-supplied values, so `IF(a>b, 5, 10)` is numeric, not +boolean. + +### Examples + +| Expression | Returns | Reason | +|------------|---------|--------| +| `1+1=2+2` | `"false"` | Parses as `(1+1) == (2+2)`; lone `=` is equality. | +| `1+1=2` | `"true"` | Same path; `2 == 2` is true. | +| `(1==1)` | `"true"` | Outer parens stripped before the lexical scan. | +| `ISEVEN(4)` | `"true"` | Top-level call to a predicate function. | +| `1 && 0` | `"false"` | Logical AND at depth 0. | +| `!0` | `"true"` | Unary logical-NOT. | +| `IF(1>2, 100, 200)` | `"200"` | `IF` is not a predicate; `>` is inside parens. | +| `1 + (1==1)` | `"2"` | `==` is inside parens, and the result isn't 0/1. | +| `(1==1) * (2==2)` | `"1"` | No comparison at depth 0; result is arithmetic. | +| `0/0 == 1` | `"nan"` | Non-finite result bypasses classification. | +| `2*PI` | `"6.28318530717959"` | No logical operator; `%.15g` format. | + +> **Why both checks?** A purely value-based test would mis-label `1+0` as +> a boolean. A purely lexical test would mis-label `IF(a>b, 5, 10)` +> (which evaluates to `5` or `10`, not `0`/`1`). The intersection is much +> closer to user intent. `te_interp()` and `te_compile()` remain +> unchanged for callers that prefer to handle classification themselves. + +--- + ## Notes - All function names are **case-insensitive**. diff --git a/src/Notepad3.c b/src/Notepad3.c index a2447c7eec..743f636517 100644 --- a/src/Notepad3.c +++ b/src/Notepad3.c @@ -211,8 +211,9 @@ static bool s_bUndoRedoScroll = false; // Auto-scroll state moved to Notepad3Util.c // for tiny expression calculation -static double s_dExpression = 0.0; -static te_int_t s_iExprError = -1; +static double s_dExpression = 0.0; +static te_int_t s_iExprError = -1; +static bool s_bExprIsLogical = false; // TinyExpr++ output mode (process-local, cycled by double-click on STATUS_TINYEXPR) typedef enum TE_OUT_MODE_T { @@ -614,9 +615,9 @@ static inline void ResetFileObservationData(const bool bResetEvt) { } // ---------------------------------------------------------------------------- -#define TE_ZERO (1.0E-8) -#define TE_FMTA "%.8G" -#define TE_FMTW L"%.8G" +#define TE_ZERO (1.0E-15) +#define TE_FMTA "%.15g" +#define TE_FMTW L"%.15g" // Bounds for safe double -> signed-integer cast. (double)INT_MAX/INT64_MAX may // round UP to the next power of two, so use literals strictly below 2^31 / 2^63. @@ -694,8 +695,14 @@ static void _FormatBinW(LPWSTR pszDest, size_t cchDest, unsigned __int64 u, int pszDest[pos] = L'\0'; } -void TinyExprToStringA(LPSTR pszDest, size_t cchDest, const double dExprEval) +void TinyExprToStringA(LPSTR pszDest, size_t cchDest, const double dExprEval, const bool bIsLogical) { + // Logical expression with a finite 0/1 result -> render as true/false, + // overriding hex/binary output modes (booleans are not a numeric value). + if (bIsLogical && isfinite(dExprEval) && (dExprEval == 0.0 || dExprEval == 1.0)) { + StringCchCopyA(pszDest, cchDest, (dExprEval == 1.0) ? "true" : "false"); + return; + } if ((s_iTinyExprOutMode != TE_OUT_DEC) && isfinite(dExprEval) && (fabs(dExprEval) < _TinyExprIntBound())) { int const width = _TinyExprBitWidth(); __int64 const i64 = (__int64)llround(dExprEval); @@ -717,7 +724,7 @@ void TinyExprToStringA(LPSTR pszDest, size_t cchDest, const double dExprEval) double intpart = 0.0; double const fracpart = modf(dExprEval, &intpart); if ((fabs(fracpart) < TE_ZERO) && (fabs(intpart) < 1.0E+21)) { - StringCchPrintfA(pszDest, cchDest, "%.21G", intpart); // integer full number display + StringCchPrintfA(pszDest, cchDest, "%.21g", intpart); // integer full number display } else { StringCchPrintfA(pszDest, cchDest, TE_FMTA, dExprEval); @@ -725,8 +732,12 @@ void TinyExprToStringA(LPSTR pszDest, size_t cchDest, const double dExprEval) } // ---------------------------------------------------------------------------- -void TinyExprToString(LPWSTR pszDest, size_t cchDest, const double dExprEval) +void TinyExprToString(LPWSTR pszDest, size_t cchDest, const double dExprEval, const bool bIsLogical) { + if (bIsLogical && isfinite(dExprEval) && (dExprEval == 0.0 || dExprEval == 1.0)) { + StringCchCopy(pszDest, cchDest, (dExprEval == 1.0) ? L"true" : L"false"); + return; + } if ((s_iTinyExprOutMode != TE_OUT_DEC) && isfinite(dExprEval) && (fabs(dExprEval) < _TinyExprIntBound())) { int const width = _TinyExprBitWidth(); __int64 const i64 = (__int64)llround(dExprEval); @@ -748,7 +759,7 @@ void TinyExprToString(LPWSTR pszDest, size_t cchDest, const double dExprEval) double intpart = 0.0; double const fracpart = modf(dExprEval, &intpart); if ((fabs(fracpart) < TE_ZERO) && (fabs(intpart) < 1.0E+21)) { - StringCchPrintf(pszDest, cchDest, L"%.21G", intpart); // integer full number display + StringCchPrintf(pszDest, cchDest, L"%.21g", intpart); // integer full number display } else { StringCchPrintf(pszDest, cchDest, TE_FMTW, dExprEval); @@ -767,7 +778,7 @@ static VOID CALLBACK TinyExprCopyTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEven char chExpr[80] = { '\0' }; if (s_iExprError == 0) { - TinyExprToStringA(chExpr, COUNTOF(chExpr), s_dExpression); + TinyExprToStringA(chExpr, COUNTOF(chExpr), s_dExpression, s_bExprIsLogical); } else if (s_iExprError > 0) { StringCchPrintfA(chExpr, COUNTOF(chExpr), "%s^[" TE_INT_FMT "]", s_pszTinyExprModePrefixA[s_iTinyExprOutMode], s_iExprError); @@ -3001,8 +3012,14 @@ static bool _EvalTinyExpr(bool qmark) double dExprEval = 0.0; te_int_t exprErr = 1; + bool bExprIsLogical = false; while (*p && exprErr) { dExprEval = te_interp(p, &exprErr); + if (!exprErr) { + // `p` hasn't moved yet (the advance happens in the inner + // while below); safe to classify the just-evaluated text. + bExprIsLogical = (te_is_logical_expr(p) != 0); + } // proceed to next possible expression while (*++p && exprErr && !(te_is_num(p) || te_is_op(p))) {} } @@ -3010,7 +3027,7 @@ static bool _EvalTinyExpr(bool qmark) if (!exprErr) { char chExpr[80] = { '\0' }; - TinyExprToStringA(chExpr, COUNTOF(chExpr), dExprEval); + TinyExprToStringA(chExpr, COUNTOF(chExpr), dExprEval, bExprIsLogical); SciCall_ReplaceSel(""); SciCall_SetSel(posBegin, posSelStart); SciCall_ReplaceSel(chExpr); @@ -11526,7 +11543,8 @@ static void _UpdateStatusbarDelayed(bool bForceRedraw) if (g_iStatusbarVisible[STATUS_TINYEXPR]) { static WCHAR tchExpression[80] = { L'\0' }; // fits "0b" + 64 bits + NUL with headroom static te_int_t s_iExErr = -3; - s_dExpression = 0.0; + s_dExpression = 0.0; + s_bExprIsLogical = false; StringCchPrintf(tchExpression, COUNTOF(tchExpression), L"%s--", s_pszTinyExprModePrefixW[s_iTinyExprOutMode]); @@ -11546,11 +11564,15 @@ static void _UpdateStatusbarDelayed(bool bForceRedraw) WideCharToMultiByte(1252, (WC_COMPOSITECHECK | WC_DISCARDNS), wchSelBuf, -1, chSeBuf, LARGE_BUFFER, &defchar, NULL); StrDelChrA(chSeBuf, chr_currency); - s_dExpression = te_interp(chSeBuf, &s_iExprError); + s_dExpression = te_interp(chSeBuf, &s_iExprError); + s_bExprIsLogical = (s_iExprError == 0) && (te_is_logical_expr(chSeBuf) != 0); } else { s_iExprError = -1; } } else if (Sci_IsMultiOrRectangleSelection() && !bIsSelectionEmpty) { + // Multi-/rect-selection concatenates fragments into a synthesized + // expression; the user-typed source isn't preserved, so don't + // try to classify it as logical. s_dExpression = _InterpMultiSelectionTinyExpr(&s_iExprError); } else { s_iExprError = -2; @@ -11560,7 +11582,7 @@ static void _UpdateStatusbarDelayed(bool bForceRedraw) } if (!s_iExprError) { - TinyExprToString(tchExpression, COUNTOF(tchExpression), s_dExpression); + TinyExprToString(tchExpression, COUNTOF(tchExpression), s_dExpression, s_bExprIsLogical); } else if (s_iExprError > 0) { StringCchPrintf(tchExpression, COUNTOF(tchExpression), L"%s^[" _W(TE_INT_FMT) L"]", s_pszTinyExprModePrefixW[s_iTinyExprOutMode], s_iExprError); diff --git a/src/tinyexprcpp/tinyexpr_cif.cpp b/src/tinyexprcpp/tinyexpr_cif.cpp index 0f91f13a6f..ae26e165c9 100644 --- a/src/tinyexprcpp/tinyexpr_cif.cpp +++ b/src/tinyexprcpp/tinyexpr_cif.cpp @@ -14,6 +14,8 @@ #include "tinyexpr.h" // C++ TinyExpr++ header #include +#include +#include #include #include #include @@ -134,6 +136,212 @@ static std::string te_cif_rewrite_binary_literals(const char *expression) return out; } +// --------------------------------------------------------------------------- +// Boolean-expression detection (for te_interp_str) +// +// An expression is classified as "logical" when, at parenthesis depth 0, +// it contains any of: +// - relational / equality / logical operators: +// `==` `=` `!=` `<>` `<=` `>=` `<` `>` `&&` `||` `!` (unary or `!=`) +// - the bare keywords `true` / `false` +// - an outermost call to AND, OR, NOT, ISERR, ISERROR, ISNA, ISNAN, +// ISEVEN, ISODD +// IF / IFS are intentionally excluded - they return arbitrary user values +// (`IF(a>b, 5, 10)` is not a boolean expression even though `a>b` is). +// +// Block / line comments are skipped. Bit-shift `<<`, `>>` and bit-rotate +// `<<<`, `>>>` are explicitly consumed without triggering classification. +// --------------------------------------------------------------------------- +namespace { + +constexpr std::array kLogicalFunctions = { + "and", "or", "not", + "iserr", "iserror", "isna", "isnan", + "iseven", "isodd", +}; + +inline bool te_is_ident_char(unsigned char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '.'; +} + +inline char te_ascii_tolower(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c + 32) : c; +} + +// Case-insensitive ASCII match of `kw` (lowercase) against expr[pos..]. +// Returns the matched length (> 0) on success and only if the byte just +// past the match is NOT an identifier continuation. Returns 0 on no match. +size_t te_match_ci(const std::string &expr, size_t pos, const char *kw) +{ + size_t i = 0; + while (kw[i]) { + if (pos + i >= expr.size() || te_ascii_tolower(expr[pos + i]) != kw[i]) { + return 0; + } + ++i; + } + if (pos + i < expr.size() && + te_is_ident_char(static_cast(expr[pos + i]))) { + return 0; + } + return i; +} + +bool te_is_logical_keyword_at(const std::string &expr, size_t pos) +{ + return te_match_ci(expr, pos, "true") > 0 || te_match_ci(expr, pos, "false") > 0; +} + +bool te_is_logical_func_at(const std::string &expr, size_t pos) +{ + for (const char *name : kLogicalFunctions) { + size_t const consumed = te_match_ci(expr, pos, name); + if (consumed == 0) { + continue; + } + size_t after = pos + consumed; + while (after < expr.size() && (expr[after] == ' ' || expr[after] == '\t')) { + ++after; + } + if (after < expr.size() && expr[after] == '(') { + return true; + } + } + return false; +} + +// Strip whitespace and any fully-enclosing outer parenthesis pairs so that +// `(1 == 1)` is classified the same as `1 == 1`. +std::string te_strip_outer_parens(std::string s) +{ + auto trim = [](std::string &t) { + size_t a = 0; + while (a < t.size() && std::isspace(static_cast(t[a]))) ++a; + size_t b = t.size(); + while (b > a && std::isspace(static_cast(t[b - 1]))) --b; + t.assign(t, a, b - a); + }; + trim(s); + while (s.size() >= 2 && s.front() == '(' && s.back() == ')') { + // Find the close-paren that balances the leading '('. If it isn't + // at the very end, the outer parens don't fully wrap (e.g. + // "(a)+(b)") - or the parens are unbalanced - so stop. + int depth = 0; + size_t closePos = std::string::npos; + for (size_t i = 0; i < s.size(); ++i) { + if (s[i] == '(') { + ++depth; + } + else if (s[i] == ')' && --depth == 0) { + closePos = i; + break; + } + } + if (closePos != s.size() - 1) { + break; + } + s.assign(s, 1, s.size() - 2); + trim(s); + } + return s; +} + +bool te_expression_is_logical(const std::string &raw) +{ + const std::string expr = te_strip_outer_parens(raw); + int depth = 0; + bool after_ident = false; + const size_t n = expr.size(); + for (size_t i = 0; i < n; ++i) { + const char c = expr[i]; + const char nxt = (i + 1 < n) ? expr[i + 1] : '\0'; + + // Skip C/C++ comments (mirrors TinyExpr++ parser behavior). + if (c == '/' && nxt == '*') { + size_t e = expr.find("*/", i + 2); + i = (e == std::string::npos) ? n - 1 : e + 1; + after_ident = false; + continue; + } + if (c == '/' && nxt == '/') { + size_t e = expr.find_first_of("\r\n", i + 2); + i = (e == std::string::npos) ? n - 1 : e - 1; + after_ident = false; + continue; + } + + if (c == '(') { + ++depth; + after_ident = false; + continue; + } + if (c == ')') { + if (depth > 0) { + --depth; + } + after_ident = false; + continue; + } + + if (depth == 0) { + // `<<`, `>>` (shift) and `<<<`, `>>>` (rotate) are NOT boolean + // producers; consume and keep scanning. Lone `<` / `>` and the + // `<=`, `>=`, `<>` variants DO classify as logical and fall + // through to the return below. + if ((c == '<' || c == '>') && c == nxt) { + ++i; + if (i + 1 < n && expr[i + 1] == c) { + ++i; + } + after_ident = false; + continue; + } + + // Relational / equality / logical operators. + if (c == '=' || c == '<' || c == '>' || c == '!') { + return true; + } + if ((c == '&' && nxt == '&') || (c == '|' && nxt == '|')) { + return true; + } + + // Identifier start: check for boolean keyword / boolean function call. + if (!after_ident && te_is_ident_char(static_cast(c)) && + !(c >= '0' && c <= '9')) { + if (te_is_logical_keyword_at(expr, i) || + te_is_logical_func_at(expr, i)) { + return true; + } + } + } + + after_ident = te_is_ident_char(static_cast(c)); + } + return false; +} + +// Mirrors Notepad3's TinyExprToStringA: integer-like values (fractional part +// below 1e-15 and magnitude under 1e21) use `%.21g`; everything else - +// including non-finite NaN / Inf / -Inf - falls through to `%.15g`, which +// snprintf renders as "nan" / "inf" / "-inf". Hex / binary output modes are +// UI-level concerns and remain in TinyExprToStringA proper. +void te_format_number(char *buf, size_t bufSize, double v) +{ + double intpart = 0.0; + double const fracpart = std::modf(v, &intpart); + if (std::fabs(fracpart) < 1.0E-15 && std::fabs(intpart) < 1.0E+21) { + std::snprintf(buf, bufSize, "%.21g", intpart); + } + else { + std::snprintf(buf, bufSize, "%.15g", v); + } +} + +} // anonymous namespace + // --------------------------------------------------------------------------- // Helper: map TinyExpr++ error state to old 1-based error position // Old convention: 0 = success, >= 1 = 1-based error position @@ -166,13 +374,16 @@ double te_interp(const char *expression, te_int_t *error) return std::numeric_limits::quiet_NaN(); } - te_parser parser; - te_cif_add_compat_functions(parser); - te_cif_configure_separators(parser); - std::string const rewritten = te_cif_rewrite_binary_literals(expression); - double result; try { - result = parser.evaluate(rewritten); + te_parser parser; + te_cif_add_compat_functions(parser); + te_cif_configure_separators(parser); + std::string const rewritten = te_cif_rewrite_binary_literals(expression); + double const result = parser.evaluate(rewritten); + if (error) { + *error = map_error(parser); + } + return result; } catch (...) { if (error) { @@ -180,11 +391,67 @@ double te_interp(const char *expression, te_int_t *error) } return std::numeric_limits::quiet_NaN(); } +} - if (error) { - *error = map_error(parser); +// Evaluates expression and returns a cooked string. +// Returns "true" / "false" when the source is lexically logical AND the +// result is finite and exactly 1.0 / 0.0; otherwise returns a numeric +// formatting (or "nan" / "inf" / "-inf"). +const char *te_interp_str(const char *expression, te_int_t *error) +{ + static thread_local char buf[64]; + constexpr double kNaN = std::numeric_limits::quiet_NaN(); + + if (!expression || !*expression) { + if (error) { + *error = 1; + } + te_format_number(buf, sizeof(buf), kNaN); + return buf; + } + + try { + te_parser parser; + te_cif_add_compat_functions(parser); + te_cif_configure_separators(parser); + std::string const rewritten = te_cif_rewrite_binary_literals(expression); + double const result = parser.evaluate(rewritten); + if (error) { + *error = map_error(parser); + } + + if (parser.success() && std::isfinite(result) && + (result == 0.0 || result == 1.0) && + te_expression_is_logical(rewritten)) { + std::snprintf(buf, sizeof(buf), "%s", result == 1.0 ? "true" : "false"); + return buf; + } + + te_format_number(buf, sizeof(buf), result); + return buf; + } + catch (...) { + if (error) { + *error = 1; + } + te_format_number(buf, sizeof(buf), kNaN); + return buf; + } +} + +// Lexical-only predicate: does this expression look logical? +// See header for the full set of detected operators / keywords / functions. +int te_is_logical_expr(const char *expression) +{ + if (!expression || !*expression) { + return 0; + } + try { + return te_expression_is_logical(std::string(expression)) ? 1 : 0; + } + catch (...) { + return 0; } - return result; } // Compiles an expression with bound variables. Returns NULL on error. diff --git a/src/tinyexprcpp/tinyexpr_cif.h b/src/tinyexprcpp/tinyexpr_cif.h index a2c931e5c3..a57babf88e 100644 --- a/src/tinyexprcpp/tinyexpr_cif.h +++ b/src/tinyexprcpp/tinyexpr_cif.h @@ -41,6 +41,38 @@ typedef struct te_variable { * parse error on failure. */ double te_interp(const char *expression, te_int_t *error); +/* Parses, evaluates, and returns a cooked string representation. + * - "true" / "false" if the expression is "logical" AND the result + * is finite and exactly 1.0 or 0.0. + * - Numeric formatting otherwise, matching Notepad3's TinyExprToStringA: + * near-integer values (fractional part below 1e-15 and magnitude + * under 1e21) use `%.21g`, all other (and non-finite) values use + * `%.15g`, which snprintf renders as "nan" / "inf" / "-inf" for + * NaN / +Inf / -Inf respectively. + * + * "Logical" detection is lexical at parenthesis depth 0: the expression + * is logical if it contains one of `==`, `=`, `!=`, `<>`, `<=`, `>=`, + * `<`, `>`, `&&`, `||`, leading `!`, the bare keywords `true`/`false`, + * or an outermost call to AND, OR, NOT, ISERR, ISERROR, ISNA, ISNAN, + * ISEVEN, or ISODD (case-insensitive). IF / IFS are intentionally NOT + * treated as logical since they return arbitrary user-supplied values. + * + * The returned pointer is to a thread-local internal buffer; it remains + * valid until the next call from the same thread. + * + * *error follows the same convention as te_interp(): 0 on success, + * 1-based parse error position on failure. */ +const char *te_interp_str(const char *expression, te_int_t *error); + +/* Returns 1 if `expression` lexically looks like a logical/comparison + * expression at parenthesis depth 0 (one fully-enclosing outer pair is + * stripped first). Considered logical when it contains one of `==`, `=`, + * `!=`, `<>`, `<=`, `>=`, `<`, `>`, `&&`, `||`, leading `!`, the bare + * keywords `true` / `false`, or an outermost call to AND, OR, NOT, + * ISERR, ISERROR, ISNA, ISNAN, ISEVEN, ISODD (case-insensitive). + * Returns 0 otherwise. Does NOT evaluate the expression. */ +int te_is_logical_expr(const char *expression); + /* Parses the input expression and binds variables. * Returns NULL on error. * *error is set to 0 on success, or the 1-based error position on failure. */ diff --git a/test/test_files/calculation/tiny_expr.cpp b/test/test_files/calculation/tiny_expr.cpp new file mode 100644 index 0000000000..54298d7f15 --- /dev/null +++ b/test/test_files/calculation/tiny_expr.cpp @@ -0,0 +1,391 @@ +/* + * TinyExpr++ Expression Test File for Notepad3 + * ============================================= + * + * How to use: + * 1. Open this file in Notepad3. + * 2. Enable Settings -> "Evaluate TinyExpr on Selection". + * 3. For any test line: place the caret IMMEDIATELY after '=?' and press + * ENTER (the trigger replaces '=?' inline with the result), OR + * select the expression and read the status-bar TinyExpr field. + * 4. Compare against the expected value shown in the trailing comment. + * + * Notes on output format (post-change): + * * Numeric values use '%.15g' (decimal) and '%.21g' (integer-like, when + * fractional part is below 1.0e-15 and magnitude is below 1.0e+21). + * * Non-finite values render as 'nan' / 'inf' / '-inf' (lowercase). + * * Boolean-detected expressions render as 'true' / 'false': + * - Lexical hit at parenthesis depth 0 on one of: + * == = != <> <= >= < > && || ! (or top-level call to + * AND / OR / NOT / ISERR / ISERROR / ISNA / ISNAN / ISEVEN / ISODD, + * or the bare keywords true / false) + * AND + * - Evaluated result is finite and exactly 0.0 or 1.0. + * * Hex / binary status-bar output modes apply to NUMERIC results only; + * boolean results override the mode and always show as 'true' / 'false'. + */ + +// Document-level commentary uses '//' or '/* ... */' C++-style +// comments, which the TinyExpr++ parser also recognizes and strips. +// Each test line is an expression terminated by '=?' (the inline- +// evaluation trigger) followed by '// ' so the +// intended result stays visible after the trigger replaces '=?'. +// +// Opens in Notepad3 with the C/C++ lexer for syntax highlighting. + + +// ============================================================ +// 1. Basic arithmetic +// ============================================================ + +1+1=? // 2 +10-3=? // 7 +6*7=? // 42 +20/4=? // 5 +10%3=? // 1 (modulus, not percent) +2^10=? // 1024 +2**10=? // 1024 (alternative power syntax) +-5+3=? // -2 ++5-3=? // 2 +1+2+3+4+5=? // 15 + + +// ============================================================ +// 2. Operator precedence +// ============================================================ + +5+5+5/2=? // 12.5 (division before addition) +(5+5+5)/2=? // 7.5 +2+5^2=? // 27 (exponentiation before addition) +(2+5)^2=? // 49 +2*3+4=? // 10 +2*(3+4)=? // 14 +~5=? // -6 (bitwise NOT - requires TE_BITWISE_OPERATORS) + + +// ============================================================ +// 3. Number formats +// ============================================================ + +42=? // 42 +3.14=? // 3.14 +.5=? // 0.5 +0x1F=? // 31 (hexadecimal) +0xFF=? // 255 +0xFFFF=? // 65535 +0b101010=? // 42 (binary - NP3 extension) +0b11111111=? // 255 +1e3=? // 1000 (scientific) +2.5e-2=? // 0.025 +1.5e10=? // 15000000000 + + +// ============================================================ +// 4. Basic math functions +// ============================================================ + +ABS(-5)=? // 5 +ABS(7)=? // 7 +CEIL(2.3)=? // 3 +CEIL(-2.3)=? // -2 +FLOOR(2.7)=? // 2 +FLOOR(-2.7)=? // -3 +ROUND(3.456, 2)=? // 3.46 +ROUND(2.5, 0)=? // 3 +ROUND(-2.5, 0)=? // -3 +TRUNC(3.7)=? // 3 +TRUNC(-3.7)=? // -3 +SIGN(-7)=? // -1 +SIGN(7)=? // 1 +SIGN(0)=? // 0 +CLAMP(15, 0, 10)=? // 10 +CLAMP(-5, 0, 10)=? // 0 +CLAMP(5, 0, 10)=? // 5 +EVEN(3)=? // 4 +EVEN(-3)=? // -4 +ODD(4)=? // 5 + + +// ============================================================ +// 5. Powers and roots +// ============================================================ + +SQRT(16)=? // 4 +SQRT(2)=? // ~1.4142135623731 +SQRT(0)=? // 0 +POW(2, 10)=? // 1024 +POW(2, 0)=? // 1 +POW(0, 0)=? // 1 +POWER(3, 4)=? // 81 +EXP(0)=? // 1 +EXP(1)=? // ~2.71828182845905 + + +// ============================================================ +// 6. Logarithms +// ============================================================ + +LN(E)=? // 1 +LN(1)=? // 0 +LOG10(1000)=? // 3 +LOG10(1)=? // 0 +LOG10(100000)=? // 5 +LOG(100)=? // 2 (LOG = LOG10 for compatibility) +log(1000)=? // 3 (lowercase 'log' - NP3 compat shim) + + +// ============================================================ +// 7. Trigonometry (angles in radians) +// ============================================================ + +SIN(0)=? // 0 +SIN(PI/2)=? // 1 +COS(0)=? // 1 +COS(PI)=? // -1 +TAN(0)=? // 0 +ASIN(1)=? // ~1.5707963267949 (= PI/2) +ACOS(1)=? // 0 +ATAN(1)=? // ~0.7853981633974 (= PI/4) +ATAN2(1, 1)=? // ~0.7853981633974 (= PI/4) +ATAN2(1, 0)=? // ~1.5707963267949 (= PI/2) +SINH(0)=? // 0 +COSH(0)=? // 1 + + +// ============================================================ +// 8. Statistics (variadic - up to 24 args) +// ============================================================ + +SUM(1, 2, 3)=? // 6 +SUM(1, 2, 3, 4, 5)=? // 15 +SUM(1.5, 2.5, 3)=? // 7 +AVERAGE(2, 4, 6)=? // 4 +AVERAGE(1, 2, 3, 4, 5)=? // 3 +MIN(3, 1, 2)=? // 1 +MIN(-5, -10, -2)=? // -10 +MAX(3, 1, 2)=? // 3 +MAX(-5, -10, -2)=? // -2 + + +// ============================================================ +// 9. Combinatorics +// ============================================================ + +FAC(0)=? // 1 +FAC(5)=? // 120 +FACT(6)=? // 720 +COMBIN(5, 2)=? // 10 +COMBIN(10, 3)=? // 120 +NCR(5, 2)=? // 10 (alias for COMBIN) +PERMUT(5, 2)=? // 20 +PERMUT(10, 3)=? // 720 +NPR(5, 2)=? // 20 (alias for PERMUT) +TGAMMA(5)=? // 24 (= 4!) +GAMMA(6)=? // 120 (= 5!) + + +// ============================================================ +// 10. Constants +// ============================================================ + +PI=? // 3.14159265358979 +E=? // 2.71828182845905 +TRUE=? // 1 +FALSE=? // 0 +NAN=? // nan + + +// ============================================================ +// 11. NEW: Boolean detection - relational / equality operators +// Result renders as 'true' / 'false' (lowercase). +// ============================================================ + +1==1=? // true +1==2=? // false +1=1=? // true (lone '=' parses as '==') +1=2=? // false (lone '=' parses as '==') +1+1=2+2=? // false ((1+1) == (2+2) -> 2 == 4) +1+1=2=? // true (parser-side, the surprising bit) +1!=2=? // true +1!=1=? // false +1<>2=? // true (<> is alternative inequality) +1<>1=? // false +5<10=? // true +5>10=? // false +5<=5=? // true +5>=5=? // true +5<5=? // false +5>5=? // false + + +// ============================================================ +// 12. NEW: Boolean detection - logical operators / functions +// ============================================================ + +1 && 1=? // true +1 && 0=? // false +0 && 0=? // false +1 || 0=? // true +0 || 0=? // false +!0=? // true +!1=? // false +AND(1, 1, 1)=? // true +AND(1, 0, 1)=? // false +OR(0, 0, 1)=? // true +OR(0, 0, 0)=? // false +NOT(0)=? // true +NOT(1)=? // false +TRUE && TRUE=? // true +FALSE || TRUE=? // true + + +// ============================================================ +// 13. Boolean detection - the depth-0 rule +// Operators inside parens DON'T count - except a single fully- +// enclosing outer pair is stripped first. +// ============================================================ + +(1==1)=? // true (outer parens stripped) +((1==1))=? // true (recursive stripping) +1+(1==1)=? // 2 (== is inside parens; result not 0/1) +(1==1)*(2==2)=? // 1 (numeric; no depth-0 op) +(1==1)+(0==1)=? // 1 (numeric; no depth-0 op) + + +// ============================================================ +// 14. Conditionals - NOT detected as boolean +// IF / IFS return arbitrary user-supplied branches; they are +// intentionally NOT in the predicate list, so results render +// as numeric values even when the condition is logical. +// ============================================================ + +IF(1>0, 100, 200)=? // 100 +IF(1<0, 100, 200)=? // 200 +IF(1==1, 42, 99)=? // 42 +IF(AND(5>1, 5<10), 1, 0)=? // 1 (numeric; outer is IF, not AND) +IFS(0, 1, 1, 2)=? // 2 +IFS(90>=90, 4, 90>=80, 3, 90>=70, 2, 1, 1)=? // 4 + + +// ============================================================ +// 15. Error checking +// ============================================================ + +NA()=? // nan +ISERR(NAN)=? // true +ISERR(5)=? // false +ISERROR(0/0)=? // true +ISNA(NAN)=? // true +ISNAN(1.5)=? // false +ISEVEN(4)=? // true +ISEVEN(3)=? // false +ISEVEN(0)=? // true +ISODD(5)=? // true +ISODD(4)=? // false + + +// ============================================================ +// 16. Bitwise functions +// ============================================================ + +BITAND(0xF0, 0x3C)=? // 48 (= 0x30) +BITAND(0b1100, 0b1010)=? // 8 (= 0b1000) +BITOR(0xF0, 0x0F)=? // 255 (= 0xFF) +BITOR(0b1100, 0b0011)=? // 15 +BITXOR(0xFF, 0xAA)=? // 85 (= 0x55) +BITXOR(0b1010, 0b1010)=? // 0 +BITNOT(0)=? // 4294967295 (32-bit ~0 = 0xFFFFFFFF) - bit-width depends on build +BITLSHIFT(1, 4)=? // 16 +BITLSHIFT(1, 20)=? // 1048576 (= 1 MiB) +BITRSHIFT(256, 4)=? // 16 +BITRSHIFT(0xFF00, 8)=? // 255 + + +// ============================================================ +// 17. Bit-shift / bit-rotate operators +// ============================================================ + +1<<4=? // 16 +256>>4=? // 16 +0xFF<<8=? // 65280 (= 0xFF00) +0xFF00>>8=? // 255 (= 0xFF) + + +// ============================================================ +// 18. Precision and rounding (new %.15g format) +// ============================================================ + +0.1+0.2=? // 0.3 (IEEE-754 surprise hidden by %.15g) +1/3=? // 0.333333333333333 +2/3=? // 0.666666666666667 +100/2.54=? // 39.3700787401575 (cm to inches) +2*PI=? // 6.28318530717959 +2*PI*6.371e6=? // 40030173.5921478 (Earth's circumference, meters) +1.0000000001=? // 1.0000000001 (preserved: > 1e-15 cutoff) +1.0000000000000001=? // 1 (clipped: <= 1e-15) +2^53=? // 9007199254740992 (largest exact integer in double) +2^60=? // 1152921504606846976 (still exact via %.21g) + + +// ============================================================ +// 19. Non-finite results +// ============================================================ + +0/0=? // nan +1/0=? // inf +-1/0=? // -inf +SQRT(-1)=? // nan +LN(0)=? // -inf +LN(-1)=? // nan +NAN+1=? // nan +NAN==NAN=? // nan (comparison involving NaN -> NaN, NOT 'false') +NAN==NAN || TRUE=? // true (short-circuit through TRUE keyword) + + +// ============================================================ +// 20. Comments inside expressions +// ============================================================ + +(3 + 4) /* this is a block comment */ * 2=? // 14 +5 + /* inline */ 3=? // 8 +2 /* multi + line */ + 3=? // 5 +// Note: line-style '//' comments at the END of the line are tricky - +// the '=?' trigger doesn't know about comments, so put '=?' BEFORE +// any '//' annotation (which is the convention used throughout this file). + + +// ============================================================ +// 21. Compound real-world expressions +// ============================================================ + +256*1024=? // 262144 (1 MiB in bytes) +SQRT(3^2 + 4^2)=? // 5 (Pythagorean) +(98.6 - 32) * 5/9=? // 37 (F -> C) +72 * 0.0254=? // 1.8288 (inches -> meters) +2^16 - 1=? // 65535 (uint16 max) +2^32 - 1=? // 4294967295 (uint32 max) +IF(5>3, MAX(1,2), MIN(3,4))=? // 2 +SUM(1,2,3) + AVERAGE(4,6)=? // 11 (= 6 + 5) +ABS(SIN(PI))<1e-10=? // true (PI is approximate -> SIN(PI) is tiny non-zero) + + +// ============================================================ +// 22. Hex / binary output modes +// +// These tests illustrate how the status-bar TinyExpr field changes +// numeric formatting based on its mode (double-click the status +// field to cycle: Decimal -> Hex -> Binary). Boolean results +// OVERRIDE the mode and always show as 'true' / 'false'. +// ============================================================ + +255=? // dec: 255 hex: 0xFF bin: 0b11111111 +4096=? // dec: 4096 hex: 0x1000 bin: 0b1000000000000 +-1=? // dec: -1 hex: 0xFFFFFFFF bin: 0b11111111111111111111111111111111 +1==1=? // true (mode ignored) +1+1=2+2=? // false (mode ignored) + + +// ============================================================ +// End of test file. +// ============================================================