Merge pull request #5914 from RaiKoHoff/dev_master

feat(tinyexpr): render logical results as "true"/"false"
This commit is contained in:
Rainer Kottenhoff 2026-05-21 14:12:12 +02:00 committed by GitHub
commit 38187a1fd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 943 additions and 27 deletions

View File

@ -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`.

View File

@ -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) {

View File

@ -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**.

View File

@ -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);

View File

@ -14,6 +14,8 @@
#include "tinyexpr.h" // C++ TinyExpr++ header
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <clocale>
#include <cmath>
@ -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<const char *, 9> 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<char>(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<unsigned char>(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<unsigned char>(t[a]))) ++a;
size_t b = t.size();
while (b > a && std::isspace(static_cast<unsigned char>(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<unsigned char>(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<unsigned char>(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<double>::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<double>::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<double>::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.

View File

@ -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. */

View File

@ -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 '<expr>=?' 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 '// <expected-value>' 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.
// ============================================================