Expressionist
Header-only C++20 evaluator of algebraic expressions embedded in JSON fields
Loading...
Searching...
No Matches
expressionist.hpp
1// Expressionist -- header-only evaluator of algebraic expressions embedded in
2// nlohmann::json fields. See PURPOSE.md for the design rationale.
3//
4// Copyright 2026 Paolo Bosetti
5// SPDX-License-Identifier: Apache-2.0
6//
7// A JSON string value that begins with the tag (default "$") is treated as an
8// algebraic expression whose variables are other keys of the JSON tree. The
9// class resolves inter-variable dependencies (order independent), detects
10// circular dependencies and undefined variables, and supports the usual
11// arithmetic, comparison and logical operators plus a library of mathematical
12// functions and constants.
13
14#pragma once
15
16#include <algorithm>
17#include <cctype>
18#include <cmath>
19#include <cstdint>
20#include <deque>
21#include <functional>
22#include <map>
23#include <memory>
24#include <optional>
25#include <queue>
26#include <stdexcept>
27#include <string>
28#include <unordered_set>
29#include <variant>
30#include <vector>
31
32#include <nlohmann/json.hpp>
33
34namespace Expressionist {
35
36using json = nlohmann::json;
37
41enum class EvalMethod {
42 GRAPH,
44 RECURSIVE,
46};
47
52class ExpressionistException : public std::exception {
53public:
54 explicit ExpressionistException(std::string message)
55 : _message(std::move(message)) {}
56 const char *what() const noexcept override { return _message.c_str(); }
57
58private:
59 std::string _message;
60}; // class ExpressionistException
61
62namespace detail {
63
64// A computed value is an integer, a double, a boolean or a sequence. Preserving
65// the integer/double distinction lets "$a + b" stay integral while "$pi/3" is a
66// float, matching the behaviour documented in PURPOSE.md. A sequence (produced
67// by the range operator "start:stop[:step]") is carried as a ready-made JSON
68// array: it may be written into a field but not used in scalar arithmetic.
69using Value = std::variant<std::int64_t, double, bool, json>;
70
71inline bool is_int(const Value &v) {
72 return std::holds_alternative<std::int64_t>(v);
73}
74
75inline bool is_bool_value(const Value &v) {
76 return std::holds_alternative<bool>(v);
77}
78
79inline bool is_seq(const Value &v) { return std::holds_alternative<json>(v); }
80
81inline double to_double(const Value &v) {
82 if (std::holds_alternative<std::int64_t>(v))
83 return static_cast<double>(std::get<std::int64_t>(v));
84 if (std::holds_alternative<double>(v))
85 return std::get<double>(v);
86 if (std::holds_alternative<bool>(v))
87 return std::get<bool>(v) ? 1.0 : 0.0;
88 throw ExpressionistException("a sequence cannot be used as a number");
89}
90
91inline bool to_bool(const Value &v) {
92 if (std::holds_alternative<bool>(v))
93 return std::get<bool>(v);
94 if (std::holds_alternative<std::int64_t>(v))
95 return std::get<std::int64_t>(v) != 0;
96 if (std::holds_alternative<double>(v))
97 return std::get<double>(v) != 0.0;
98 throw ExpressionistException("a sequence cannot be used as a boolean");
99}
100
101inline json to_json(const Value &v) {
102 if (std::holds_alternative<std::int64_t>(v))
103 return json(std::get<std::int64_t>(v));
104 if (std::holds_alternative<bool>(v))
105 return json(std::get<bool>(v));
106 if (std::holds_alternative<double>(v))
107 return json(std::get<double>(v));
108 return std::get<json>(v);
109}
110
111// ---------------------------------------------------------------------------
112// Symbol tables: constants and mathematical functions.
113// ---------------------------------------------------------------------------
114
115using UnaryFn = std::function<double(double)>;
116using BinaryFn = std::function<double(double, double)>;
117
118struct Symbols {
119 std::map<std::string, double> constants;
120 std::map<std::string, UnaryFn> unary;
121 std::map<std::string, BinaryFn> binary;
122}; // struct Symbols
123
124inline Symbols default_symbols() {
125 Symbols s;
126 // Literals are used instead of M_PI/M_E for portability (MSVC does not define
127 // them without _USE_MATH_DEFINES).
128 s.constants["pi"] = 3.14159265358979323846;
129 s.constants["e"] = 2.71828182845904523536;
130 s.constants["tau"] = 6.28318530717958647692;
131
132 s.unary["sin"] = [](double x) { return std::sin(x); };
133 s.unary["cos"] = [](double x) { return std::cos(x); };
134 s.unary["tan"] = [](double x) { return std::tan(x); };
135 s.unary["asin"] = [](double x) { return std::asin(x); };
136 s.unary["acos"] = [](double x) { return std::acos(x); };
137 s.unary["atan"] = [](double x) { return std::atan(x); };
138 s.unary["sinh"] = [](double x) { return std::sinh(x); };
139 s.unary["cosh"] = [](double x) { return std::cosh(x); };
140 s.unary["tanh"] = [](double x) { return std::tanh(x); };
141 s.unary["exp"] = [](double x) { return std::exp(x); };
142 s.unary["log"] = [](double x) { return std::log(x); }; // natural log
143 s.unary["ln"] = [](double x) { return std::log(x); };
144 s.unary["log10"] = [](double x) { return std::log10(x); };
145 s.unary["log2"] = [](double x) { return std::log2(x); };
146 s.unary["sqrt"] = [](double x) { return std::sqrt(x); };
147 s.unary["cbrt"] = [](double x) { return std::cbrt(x); };
148 s.unary["abs"] = [](double x) { return std::fabs(x); };
149 s.unary["floor"] = [](double x) { return std::floor(x); };
150 s.unary["ceil"] = [](double x) { return std::ceil(x); };
151 s.unary["round"] = [](double x) { return std::round(x); };
152 s.unary["trunc"] = [](double x) { return std::trunc(x); };
153 s.unary["sign"] = [](double x) {
154 return x > 0.0 ? 1.0 : (x < 0.0 ? -1.0 : 0.0);
155 };
156
157 s.binary["pow"] = [](double a, double b) { return std::pow(a, b); };
158 s.binary["atan2"] = [](double a, double b) { return std::atan2(a, b); };
159 s.binary["hypot"] = [](double a, double b) { return std::hypot(a, b); };
160 s.binary["min"] = [](double a, double b) { return std::min(a, b); };
161 s.binary["max"] = [](double a, double b) { return std::max(a, b); };
162 s.binary["mod"] = [](double a, double b) { return std::fmod(a, b); };
163 return s;
164}
165
166// ---------------------------------------------------------------------------
167// Tokenizer
168// ---------------------------------------------------------------------------
169
170enum class TokType {
171 Number,
172 Ident,
173 Plus,
174 Minus,
175 Star,
176 Slash,
177 Caret,
178 LParen,
179 RParen,
180 Comma,
181 Lt,
182 Le,
183 Gt,
184 Ge,
185 EqEq,
186 NotEq,
187 And,
188 Or,
189 Not,
190 Colon,
191 End
192}; // enum class TokType
193
194struct Token {
195 TokType type;
196 std::string text;
197 Value value; // valid when type == Number
198 std::size_t pos; // position in the source expression
199}; // struct Token
200
201class Tokenizer {
202public:
203 explicit Tokenizer(std::string src) : _src(std::move(src)) {}
204
205 std::vector<Token> tokenize() {
206 std::vector<Token> tokens;
207 for (;;) {
208 skip_ws();
209 if (_i >= _src.size()) {
210 tokens.push_back({TokType::End, "", Value{}, _i});
211 break;
212 }
213 char c = _src[_i];
214 if (is_digit(c) ||
215 (c == '.' && _i + 1 < _src.size() && is_digit(_src[_i + 1])))
216 tokens.push_back(read_number());
217 else if (std::isalpha(static_cast<unsigned char>(c)) || c == '_')
218 tokens.push_back(read_ident());
219 else
220 tokens.push_back(read_op());
221 }
222 return tokens;
223 }
224
225private:
226 static bool is_digit(char c) {
227 return std::isdigit(static_cast<unsigned char>(c)) != 0;
228 }
229
230 void skip_ws() {
231 while (_i < _src.size() &&
232 std::isspace(static_cast<unsigned char>(_src[_i])))
233 ++_i;
234 }
235
236 Token read_number() {
237 std::size_t start = _i;
238 bool is_float = false;
239 while (_i < _src.size() && is_digit(_src[_i]))
240 ++_i;
241 if (_i < _src.size() && _src[_i] == '.') {
242 is_float = true;
243 ++_i;
244 while (_i < _src.size() && is_digit(_src[_i]))
245 ++_i;
246 }
247 if (_i < _src.size() && (_src[_i] == 'e' || _src[_i] == 'E')) {
248 is_float = true;
249 ++_i;
250 if (_i < _src.size() && (_src[_i] == '+' || _src[_i] == '-'))
251 ++_i;
252 while (_i < _src.size() && is_digit(_src[_i]))
253 ++_i;
254 }
255 std::string text = _src.substr(start, _i - start);
256 Value v;
257 if (is_float) {
258 v = std::stod(text);
259 } else {
260 try {
261 v = static_cast<std::int64_t>(std::stoll(text));
262 } catch (const std::exception &) {
263 v = std::stod(text); // out of int64 range -> keep as double
264 }
265 }
266 return {TokType::Number, text, v, start};
267 }
268
269 Token read_ident() {
270 std::size_t start = _i;
271 while (
272 _i < _src.size() &&
273 (std::isalnum(static_cast<unsigned char>(_src[_i])) || _src[_i] == '_'))
274 ++_i;
275 return {TokType::Ident, _src.substr(start, _i - start), Value{}, start};
276 }
277
278 Token read_op() {
279 std::size_t start = _i;
280 char c = _src[_i];
281 auto two = [&](char a, char b) {
282 return _i + 1 < _src.size() && _src[_i] == a && _src[_i + 1] == b;
283 };
284 if (two('<', '='))
285 return _i += 2, Token{TokType::Le, "<=", {}, start};
286 if (two('>', '='))
287 return _i += 2, Token{TokType::Ge, ">=", {}, start};
288 if (two('=', '='))
289 return _i += 2, Token{TokType::EqEq, "==", {}, start};
290 if (two('!', '='))
291 return _i += 2, Token{TokType::NotEq, "!=", {}, start};
292 if (two('&', '&'))
293 return _i += 2, Token{TokType::And, "&&", {}, start};
294 if (two('|', '|'))
295 return _i += 2, Token{TokType::Or, "||", {}, start};
296 switch (c) {
297 case '+':
298 return ++_i, Token{TokType::Plus, "+", {}, start};
299 case '-':
300 return ++_i, Token{TokType::Minus, "-", {}, start};
301 case '*':
302 return ++_i, Token{TokType::Star, "*", {}, start};
303 case '/':
304 return ++_i, Token{TokType::Slash, "/", {}, start};
305 case '^':
306 return ++_i, Token{TokType::Caret, "^", {}, start};
307 case '(':
308 return ++_i, Token{TokType::LParen, "(", {}, start};
309 case ')':
310 return ++_i, Token{TokType::RParen, ")", {}, start};
311 case ',':
312 return ++_i, Token{TokType::Comma, ",", {}, start};
313 case ':':
314 return ++_i, Token{TokType::Colon, ":", {}, start};
315 case '<':
316 return ++_i, Token{TokType::Lt, "<", {}, start};
317 case '>':
318 return ++_i, Token{TokType::Gt, ">", {}, start};
319 case '!':
320 return ++_i, Token{TokType::Not, "!", {}, start};
321 default:
322 throw ExpressionistException("Unexpected character '" +
323 std::string(1, c) + "' at position " +
324 std::to_string(start));
325 }
326 }
327
328 std::string _src;
329 std::size_t _i = 0;
330}; // class Tokenizer
331
332// ---------------------------------------------------------------------------
333// Abstract syntax tree
334// ---------------------------------------------------------------------------
335
336enum class NodeKind {
337 IntLit,
338 FloatLit,
339 BoolLit,
340 Ident,
341 Unary,
342 Binary,
343 Call,
344 Range
345};
346
347struct Node {
348 NodeKind kind;
349 Value value; // literals
350 std::string name; // Ident / Call name
351 TokType op = TokType::End; // Unary / Binary operator
352 std::vector<std::shared_ptr<Node>> children; // operands / call arguments
353}; // struct Node
354
355using NodePtr = std::shared_ptr<Node>;
356
357class Parser {
358public:
359 Parser(std::vector<Token> tokens, std::string src)
360 : _tokens(std::move(tokens)), _src(std::move(src)) {}
361
362 NodePtr parse() {
363 NodePtr n = parse_range();
364 if (!check(TokType::End))
365 throw error("unexpected trailing token");
366 return n;
367 }
368
369private:
370 const Token &peek() const { return _tokens[_pos]; }
371 const Token &advance() { return _tokens[_pos++]; }
372 bool check(TokType t) const { return peek().type == t; }
373 bool match(TokType t) {
374 if (check(t)) {
375 ++_pos;
376 return true;
377 }
378 return false;
379 }
380
381 ExpressionistException error(const std::string &why) const {
382 return ExpressionistException("Parse error in \"" + _src + "\": " + why +
383 " ('" + peek().text + "' at position " +
384 std::to_string(peek().pos) + ")");
385 }
386
387 static NodePtr make_binary(TokType op, NodePtr l, NodePtr r) {
388 auto n = std::make_shared<Node>();
389 n->kind = NodeKind::Binary;
390 n->op = op;
391 n->children = {std::move(l), std::move(r)};
392 return n;
393 }
394
395 // A range is the lowest-precedence construct and appears only at the top of
396 // an expression: "start:stop" (integer bounds, implicit step 1) or
397 // "start:stop:step" (floating). Its parts are ordinary scalar expressions.
398 NodePtr parse_range() {
399 NodePtr first = parse_or();
400 if (!check(TokType::Colon))
401 return first;
402 auto node = std::make_shared<Node>();
403 node->kind = NodeKind::Range;
404 node->children.push_back(first);
405 while (match(TokType::Colon))
406 node->children.push_back(parse_or());
407 if (node->children.size() > 3)
408 throw error("a range has at most three parts (start:stop:step)");
409 return node;
410 }
411
412 NodePtr parse_or() {
413 NodePtr n = parse_and();
414 while (check(TokType::Or)) {
415 advance();
416 n = make_binary(TokType::Or, n, parse_and());
417 }
418 return n;
419 }
420
421 NodePtr parse_and() {
422 NodePtr n = parse_equality();
423 while (check(TokType::And)) {
424 advance();
425 n = make_binary(TokType::And, n, parse_equality());
426 }
427 return n;
428 }
429
430 NodePtr parse_equality() {
431 NodePtr n = parse_comparison();
432 while (check(TokType::EqEq) || check(TokType::NotEq)) {
433 TokType op = advance().type;
434 n = make_binary(op, n, parse_comparison());
435 }
436 return n;
437 }
438
439 NodePtr parse_comparison() {
440 NodePtr n = parse_additive();
441 while (check(TokType::Lt) || check(TokType::Le) || check(TokType::Gt) ||
442 check(TokType::Ge)) {
443 TokType op = advance().type;
444 n = make_binary(op, n, parse_additive());
445 }
446 return n;
447 }
448
449 NodePtr parse_additive() {
450 NodePtr n = parse_multiplicative();
451 while (check(TokType::Plus) || check(TokType::Minus)) {
452 TokType op = advance().type;
453 n = make_binary(op, n, parse_multiplicative());
454 }
455 return n;
456 }
457
458 NodePtr parse_multiplicative() {
459 NodePtr n = parse_unary();
460 while (check(TokType::Star) || check(TokType::Slash)) {
461 TokType op = advance().type;
462 n = make_binary(op, n, parse_unary());
463 }
464 return n;
465 }
466
467 NodePtr parse_unary() {
468 if (check(TokType::Minus) || check(TokType::Not)) {
469 TokType op = advance().type;
470 auto n = std::make_shared<Node>();
471 n->kind = NodeKind::Unary;
472 n->op = op;
473 n->children = {parse_unary()};
474 return n;
475 }
476 return parse_power();
477 }
478
479 // Power binds tighter than unary minus (so -2^2 == -(2^2)) and is
480 // right-associative (2^3^2 == 2^(3^2)); its exponent may itself be unary.
481 NodePtr parse_power() {
482 NodePtr base = parse_primary();
483 if (check(TokType::Caret)) {
484 advance();
485 return make_binary(TokType::Caret, base, parse_unary());
486 }
487 return base;
488 }
489
490 NodePtr parse_primary() {
491 const Token &t = peek();
492 if (t.type == TokType::Number) {
493 advance();
494 auto n = std::make_shared<Node>();
495 n->kind = is_int(t.value) ? NodeKind::IntLit : NodeKind::FloatLit;
496 n->value = t.value;
497 return n;
498 }
499 if (t.type == TokType::LParen) {
500 advance();
501 NodePtr n = parse_or();
502 if (!match(TokType::RParen))
503 throw error("expected ')'");
504 return n;
505 }
506 if (t.type == TokType::Ident) {
507 advance();
508 if (t.text == "true" || t.text == "false") {
509 auto n = std::make_shared<Node>();
510 n->kind = NodeKind::BoolLit;
511 n->value = (t.text == "true");
512 return n;
513 }
514 if (check(TokType::LParen)) {
515 advance();
516 auto n = std::make_shared<Node>();
517 n->kind = NodeKind::Call;
518 n->name = t.text;
519 if (!check(TokType::RParen)) {
520 n->children.push_back(parse_or());
521 while (match(TokType::Comma))
522 n->children.push_back(parse_or());
523 }
524 if (!match(TokType::RParen))
525 throw error("expected ')' in call to '" + t.text + "'");
526 return n;
527 }
528 auto n = std::make_shared<Node>();
529 n->kind = NodeKind::Ident;
530 n->name = t.text;
531 return n;
532 }
533 throw error("unexpected token");
534 }
535
536 std::vector<Token> _tokens;
537 std::string _src;
538 std::size_t _pos = 0;
539}; // class Parser
540
541// Names appearing in variable position (constants included; the caller decides
542// which are keys vs. constants). Function names are not collected.
543inline void collect_idents(const NodePtr &n, std::vector<std::string> &out) {
544 if (!n)
545 return;
546 if (n->kind == NodeKind::Ident)
547 out.push_back(n->name);
548 for (const auto &c : n->children)
549 collect_idents(c, out);
550}
551
552using Resolver = std::function<std::optional<Value>(const std::string &)>;
553
554inline Value num_binary(const Value &l, const Value &r, TokType op) {
555 if (op == TokType::Slash)
556 return to_double(l) / to_double(r); // division is always floating point
557 if (op == TokType::Caret) {
558 if (is_int(l) && is_int(r) && std::get<std::int64_t>(r) >= 0) {
559 std::int64_t base = std::get<std::int64_t>(l);
560 std::int64_t exp = std::get<std::int64_t>(r);
561 std::int64_t result = 1;
562 for (std::int64_t k = 0; k < exp; ++k)
563 result *= base;
564 return result;
565 }
566 return std::pow(to_double(l), to_double(r));
567 }
568 if (is_int(l) && is_int(r)) {
569 std::int64_t a = std::get<std::int64_t>(l);
570 std::int64_t b = std::get<std::int64_t>(r);
571 switch (op) {
572 case TokType::Plus:
573 return a + b;
574 case TokType::Minus:
575 return a - b;
576 case TokType::Star:
577 return a * b;
578 default:
579 break;
580 }
581 }
582 double a = to_double(l);
583 double b = to_double(r);
584 switch (op) {
585 case TokType::Plus:
586 return a + b;
587 case TokType::Minus:
588 return a - b;
589 case TokType::Star:
590 return a * b;
591 default:
592 return 0.0; // unreachable
593 }
594}
595
596inline Value eval_node(const NodePtr &n, const Symbols &sym,
597 const Resolver &resolve) {
598 switch (n->kind) {
599 case NodeKind::IntLit:
600 case NodeKind::FloatLit:
601 case NodeKind::BoolLit:
602 return n->value;
603 case NodeKind::Ident: {
604 if (auto v = resolve(n->name)) // a key shadows a same-named constant
605 return *v;
606 auto it = sym.constants.find(n->name);
607 if (it != sym.constants.end())
608 return it->second;
609 throw ExpressionistException("Undefined variable or constant: '" + n->name +
610 "'");
611 }
612 case NodeKind::Unary: {
613 if (n->op == TokType::Not)
614 return !to_bool(eval_node(n->children[0], sym, resolve));
615 Value c = eval_node(n->children[0], sym, resolve);
616 if (is_int(c))
617 return -std::get<std::int64_t>(c);
618 return -to_double(c);
619 }
620 case NodeKind::Binary: {
621 if (n->op == TokType::And) {
622 if (!to_bool(eval_node(n->children[0], sym, resolve)))
623 return false;
624 return to_bool(eval_node(n->children[1], sym, resolve));
625 }
626 if (n->op == TokType::Or) {
627 if (to_bool(eval_node(n->children[0], sym, resolve)))
628 return true;
629 return to_bool(eval_node(n->children[1], sym, resolve));
630 }
631 Value l = eval_node(n->children[0], sym, resolve);
632 Value r = eval_node(n->children[1], sym, resolve);
633 switch (n->op) {
634 case TokType::Plus:
635 case TokType::Minus:
636 case TokType::Star:
637 case TokType::Slash:
638 case TokType::Caret:
639 return num_binary(l, r, n->op);
640 case TokType::Lt:
641 return to_double(l) < to_double(r);
642 case TokType::Le:
643 return to_double(l) <= to_double(r);
644 case TokType::Gt:
645 return to_double(l) > to_double(r);
646 case TokType::Ge:
647 return to_double(l) >= to_double(r);
648 case TokType::EqEq:
649 if (is_bool_value(l) && is_bool_value(r))
650 return std::get<bool>(l) == std::get<bool>(r);
651 return to_double(l) == to_double(r);
652 case TokType::NotEq:
653 if (is_bool_value(l) && is_bool_value(r))
654 return std::get<bool>(l) != std::get<bool>(r);
655 return to_double(l) != to_double(r);
656 default:
657 throw ExpressionistException("Internal error: bad binary operator");
658 }
659 }
660 case NodeKind::Range: {
661 if (n->children.size() == 2) {
662 Value a = eval_node(n->children[0], sym, resolve);
663 Value b = eval_node(n->children[1], sym, resolve);
664 if (!is_int(a) || !is_int(b))
665 throw ExpressionistException(
666 "Binary range 'start:stop' requires integer bounds");
667 std::int64_t start = std::get<std::int64_t>(a);
668 std::int64_t stop = std::get<std::int64_t>(b);
669 json arr = json::array();
670 for (std::int64_t v = start; v <= stop; ++v)
671 arr.push_back(v);
672 return Value(std::in_place_type<json>, std::move(arr));
673 }
674 if (n->children.size() == 3) {
675 double start = to_double(eval_node(n->children[0], sym, resolve));
676 double stop = to_double(eval_node(n->children[1], sym, resolve));
677 double step = to_double(eval_node(n->children[2], sym, resolve));
678 if (step == 0.0)
679 throw ExpressionistException("Range step must be non-zero");
680 json arr = json::array();
681 double span = stop - start;
682 // Generate only when the step points from start towards stop; a step in
683 // the wrong direction yields an empty sequence rather than diverging. The
684 // small epsilon keeps the endpoint inclusive despite rounding, so
685 // "0:1:0.1" ends exactly at 1.
686 if (span == 0.0 || (span > 0.0) == (step > 0.0)) {
687 std::int64_t count =
688 static_cast<std::int64_t>(std::floor(span / step + 1e-9));
689 for (std::int64_t i = 0; i <= count; ++i)
690 arr.push_back(start + static_cast<double>(i) * step);
691 }
692 return Value(std::in_place_type<json>, std::move(arr));
693 }
694 throw ExpressionistException(
695 "A range must have two (start:stop) or three (start:stop:step) parts");
696 }
697 case NodeKind::Call: {
698 const std::string &fn = n->name;
699 auto u = sym.unary.find(fn);
700 if (u != sym.unary.end()) {
701 if (n->children.size() != 1)
702 throw ExpressionistException("Function '" + fn +
703 "' expects 1 argument, got " +
704 std::to_string(n->children.size()));
705 return u->second(to_double(eval_node(n->children[0], sym, resolve)));
706 }
707 auto b = sym.binary.find(fn);
708 if (b != sym.binary.end()) {
709 if (n->children.size() != 2)
710 throw ExpressionistException("Function '" + fn +
711 "' expects 2 arguments, got " +
712 std::to_string(n->children.size()));
713 double x = to_double(eval_node(n->children[0], sym, resolve));
714 double y = to_double(eval_node(n->children[1], sym, resolve));
715 return b->second(x, y);
716 }
717 throw ExpressionistException("Unknown function: '" + fn + "'");
718 }
719 }
720 throw ExpressionistException("Internal error: bad node");
721}
722
723// ---------------------------------------------------------------------------
724// Evaluator: walks a JSON tree, builds scopes/cells and evaluates expressions.
725// ---------------------------------------------------------------------------
726
727class Evaluator {
728public:
729 Evaluator(const Symbols &sym, std::string tag, std::string disableKey,
730 EvalMethod method)
731 : _sym(sym), _tag(std::move(tag)), _disableKey(std::move(disableKey)),
732 _method(method) {}
733
734 void run(json &root) {
735 _cells.clear();
736 _scopes.clear();
737 walk(root, nullptr, "");
738 if (_method == EvalMethod::GRAPH)
739 eval_graph();
740 else
741 eval_recursive();
742 for (const Cell &c : _cells)
743 if (c.is_expr) // all expression cells are evaluated on success
744 *c.slot = to_json(c.result);
745 }
746
747private:
748 struct Scope {
749 Scope *parent = nullptr;
750 std::map<std::string, std::size_t> keys; // key -> cell index
751 };
752
753 struct Cell {
754 json *slot = nullptr; // location to overwrite with the computed value
755 Scope *scope = nullptr;
756 std::string name; // last path component, for diagnostics
757 std::string path; // full path, for diagnostics
758 bool is_expr = false;
759 std::string source; // expression text (tag stripped)
760 NodePtr ast;
761 Value result;
762 int state = 0; // 0 unvisited, 1 visiting, 2 done (recursive method)
763 };
764
765 bool starts_with_tag(const std::string &s) const {
766 return !_tag.empty() && s.rfind(_tag, 0) == 0;
767 }
768
769 // An object opts its whole subtree out of evaluation by carrying a
770 // `disableKey: false` member (default key: "expressionist"). The check
771 // looks only at the literal JSON value, never at an evaluated result, so it
772 // works uniformly regardless of definition order or evaluation strategy.
773 bool is_disabled(const json &value) const {
774 if (_disableKey.empty())
775 return false;
776 auto it = value.find(_disableKey);
777 return it != value.end() && *it == false;
778 }
779
780 std::size_t add_cell(json &value, Scope *scope, const std::string &path) {
781 std::size_t idx = _cells.size();
782 _cells.emplace_back();
783 Cell &cell = _cells.back();
784 cell.slot = &value;
785 cell.scope = scope;
786 cell.path = path;
787 cell.name = last_component(path);
788 if (value.is_string()) {
789 const std::string &s = value.get_ref<const std::string &>();
790 if (starts_with_tag(s)) {
791 cell.is_expr = true;
792 cell.source = s.substr(_tag.size());
793 try {
794 Tokenizer tok(cell.source);
795 Parser parser(tok.tokenize(), cell.source);
796 cell.ast = parser.parse();
797 } catch (const ExpressionistException &e) {
798 throw ExpressionistException(cell_context(cell) + e.what());
799 }
800 }
801 }
802 return idx;
803 }
804
805 void walk(json &value, Scope *enclosing, const std::string &path) {
806 if (value.is_object()) {
807 if (is_disabled(value))
808 return; // leave this object and everything under it untouched
809 _scopes.emplace_back();
810 Scope *scope = &_scopes.back();
811 scope->parent = enclosing;
812 // Register every member first so sibling references resolve regardless of
813 // definition order.
814 for (auto it = value.begin(); it != value.end(); ++it)
815 scope->keys[it.key()] =
816 add_cell(it.value(), scope, join(path, it.key()));
817 // Then descend into nested containers for their own scopes / inner cells.
818 for (auto it = value.begin(); it != value.end(); ++it)
819 if (it.value().is_object() || it.value().is_array())
820 walk(it.value(), scope, join(path, it.key()));
821 } else if (value.is_array()) {
822 // Arrays are not scopes: their elements resolve in the enclosing object.
823 for (std::size_t i = 0; i < value.size(); ++i) {
824 std::string p = path + "/" + std::to_string(i);
825 json &el = value[i];
826 if (el.is_object() || el.is_array())
827 walk(el, enclosing, p);
828 else
829 add_cell(el, enclosing, p);
830 }
831 }
832 }
833
834 // Lexical lookup: nearest enclosing object scope, then ancestors.
835 std::size_t resolve_name(const std::string &name, Scope *scope,
836 bool &found) const {
837 for (Scope *s = scope; s != nullptr; s = s->parent) {
838 auto it = s->keys.find(name);
839 if (it != s->keys.end()) {
840 found = true;
841 return it->second;
842 }
843 }
844 found = false;
845 return 0;
846 }
847
848 Value literal_value(const Cell &c) const {
849 const json &v = *c.slot;
850 if (v.is_number_integer())
851 return static_cast<std::int64_t>(v.get<std::int64_t>());
852 if (v.is_number_float())
853 return v.get<double>();
854 if (v.is_boolean())
855 return v.get<bool>();
856 throw ExpressionistException("variable '" + c.name + "' is not numeric");
857 }
858
859 std::string cell_context(const Cell &c) const {
860 return "In '" + c.path + "' (\"" + _tag + c.source + "\"): ";
861 }
862
863 // Add the offending cell's location once, avoiding duplicate prefixes as the
864 // exception unwinds through nested evaluations.
865 [[noreturn]] void
866 rethrow_with_context(const Cell &c, const ExpressionistException &e) const {
867 std::string msg = e.what();
868 if (msg.rfind("In '", 0) == 0)
869 throw e;
870 throw ExpressionistException(cell_context(c) + msg);
871 }
872
873 // --- Recursive strategy -------------------------------------------------
874
875 void eval_recursive() {
876 std::vector<std::size_t> stack;
877 for (std::size_t i = 0; i < _cells.size(); ++i)
878 if (_cells[i].is_expr)
879 eval_cell(i, stack);
880 }
881
882 Value eval_cell(std::size_t idx, std::vector<std::size_t> &stack) {
883 Cell &cell = _cells[idx];
884 if (!cell.is_expr)
885 return literal_value(cell);
886 if (cell.state == 2)
887 return cell.result;
888 if (cell.state == 1)
889 throw ExpressionistException("Circular dependency: " +
890 cycle_path(stack, idx));
891 cell.state = 1;
892 stack.push_back(idx);
893 Scope *scope = cell.scope;
894 Resolver resolver = [&](const std::string &name) -> std::optional<Value> {
895 bool found;
896 std::size_t tgt = resolve_name(name, scope, found);
897 if (!found)
898 return std::nullopt;
899 return eval_cell(tgt, stack);
900 };
901 Value v;
902 try {
903 v = eval_node(cell.ast, _sym, resolver);
904 } catch (const ExpressionistException &e) {
905 rethrow_with_context(cell, e);
906 }
907 stack.pop_back();
908 cell.result = v;
909 cell.state = 2;
910 return v;
911 }
912
913 std::string cycle_path(const std::vector<std::size_t> &stack,
914 std::size_t idx) const {
915 std::size_t start = 0;
916 for (std::size_t k = 0; k < stack.size(); ++k)
917 if (stack[k] == idx) {
918 start = k;
919 break;
920 }
921 std::string m;
922 for (std::size_t k = start; k < stack.size(); ++k)
923 m += _cells[stack[k]].name + " -> ";
924 m += _cells[idx].name;
925 return m;
926 }
927
928 // --- Graph strategy (topological order via Kahn's algorithm) ------------
929
930 void eval_graph() {
931 std::size_t n = _cells.size();
932 std::vector<std::vector<std::size_t>> dependents(n);
933 std::vector<int> indeg(n, 0);
934 std::vector<std::size_t> expr_cells;
935 for (std::size_t i = 0; i < n; ++i) {
936 if (!_cells[i].is_expr)
937 continue;
938 expr_cells.push_back(i);
939 std::vector<std::string> idents;
940 collect_idents(_cells[i].ast, idents);
941 std::unordered_set<std::size_t> seen;
942 for (const std::string &name : idents) {
943 bool found;
944 std::size_t tgt = resolve_name(name, _cells[i].scope, found);
945 if (found && _cells[tgt].is_expr && seen.insert(tgt).second) {
946 dependents[tgt].push_back(i);
947 ++indeg[i];
948 }
949 }
950 }
951 std::queue<std::size_t> q;
952 for (std::size_t i : expr_cells)
953 if (indeg[i] == 0)
954 q.push(i);
955 std::size_t processed = 0;
956 while (!q.empty()) {
957 std::size_t i = q.front();
958 q.pop();
959 ++processed;
960 eval_expr_graph(i);
961 for (std::size_t d : dependents[i])
962 if (--indeg[d] == 0)
963 q.push(d);
964 }
965 if (processed != expr_cells.size()) {
966 std::string names;
967 for (std::size_t i : expr_cells)
968 if (_cells[i].state != 2)
969 names += _cells[i].name + " ";
970 throw ExpressionistException("Circular dependency detected among: " +
971 names);
972 }
973 }
974
975 void eval_expr_graph(std::size_t idx) {
976 Cell &cell = _cells[idx];
977 Scope *scope = cell.scope;
978 Resolver resolver = [&](const std::string &name) -> std::optional<Value> {
979 bool found;
980 std::size_t tgt = resolve_name(name, scope, found);
981 if (!found)
982 return std::nullopt;
983 const Cell &t = _cells[tgt];
984 if (t.is_expr)
985 return t.result; // already evaluated: dependencies come first
986 return literal_value(t);
987 };
988 try {
989 cell.result = eval_node(cell.ast, _sym, resolver);
990 } catch (const ExpressionistException &e) {
991 rethrow_with_context(cell, e);
992 }
993 cell.state = 2;
994 }
995
996 static std::string join(const std::string &path, const std::string &key) {
997 return path + "/" + key;
998 }
999
1000 static std::string last_component(const std::string &path) {
1001 std::size_t p = path.find_last_of('/');
1002 return p == std::string::npos ? path : path.substr(p + 1);
1003 }
1004
1005 const Symbols &_sym;
1006 std::string _tag;
1007 std::string _disableKey;
1008 EvalMethod _method;
1009 std::deque<Scope> _scopes; // stable addresses for Scope*
1010 std::vector<Cell> _cells;
1011}; // class Evaluator
1012
1013} // namespace detail
1014
1019
1048public:
1050 Expressionist(json o, EvalMethod method = EvalMethod::RECURSIVE)
1051 : _object(std::move(o)), _evalMethod(method),
1052 _symbols(detail::default_symbols()) {}
1055 Expressionist(std::string s, EvalMethod method = EvalMethod::RECURSIVE)
1056 : _evalMethod(method), _symbols(detail::default_symbols()) {
1057 try {
1058 _object = json::parse(s);
1059 } catch (const std::exception &e) {
1060 throw ExpressionistException("Failed to parse JSON: " +
1061 std::string(e.what()));
1062 }
1063 }
1064 // A string literal is otherwise ambiguous between the json and std::string
1065 // constructors (each a single user-defined conversion from const char*).
1066 // This exact-match overload disambiguates in favour of JSON parsing.
1068 Expressionist(const char *s, EvalMethod method = EvalMethod::RECURSIVE)
1069 : Expressionist(std::string(s), method) {}
1072 Expressionist(EvalMethod method = EvalMethod::RECURSIVE)
1073 : _evalMethod(method), _symbols(detail::default_symbols()) {}
1074
1075 ~Expressionist() = default;
1076
1081 void evaluate() {
1082 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1083 ev.run(_object);
1084 }
1085
1096 void evaluate(json &object) const {
1097 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1098 ev.run(object);
1099 }
1100
1107 json produce() const {
1108 json copy = _object;
1109 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1110 ev.run(copy);
1111 return copy;
1112 }
1113
1116 json produce(json object) const {
1117 json copy = object;
1118 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1119 ev.run(copy);
1120 return copy;
1121 }
1122
1125 void setEvalMethod(EvalMethod method) { _evalMethod = method; }
1127 EvalMethod getEvalMethod() const { return _evalMethod; }
1128
1134 void setTag(const std::string &tag) { _tag = tag; }
1136 std::string tag() const { return _tag; }
1137
1145 void setDisableKey(const std::string &key) { _disableKey = key; }
1147 std::string disableKey() const { return _disableKey; }
1148
1150 const json &object() const { return _object; }
1151
1154 void addConstant(const std::string &name, double value) {
1155 _symbols.constants[name] = value;
1156 }
1161 void addUnaryFunction(const std::string &name, detail::UnaryFn fn) {
1162 _symbols.unary[name] = std::move(fn);
1163 }
1169 void addBinaryFunction(const std::string &name, detail::BinaryFn fn) {
1170 _symbols.binary[name] = std::move(fn);
1171 }
1172
1173private:
1174 json _object = json::object();
1175 EvalMethod _evalMethod = EvalMethod::RECURSIVE;
1176 std::string _tag = "$";
1177 std::string _disableKey = "expressionist";
1178 detail::Symbols _symbols;
1179}; // class Expressionist
1180
1181} // namespace Expressionist
Definition expressionist.hpp:52
Definition expressionist.hpp:1047
void setTag(const std::string &tag)
Definition expressionist.hpp:1134
void addUnaryFunction(const std::string &name, detail::UnaryFn fn)
Definition expressionist.hpp:1161
EvalMethod getEvalMethod() const
The currently selected resolution strategy.
Definition expressionist.hpp:1127
Expressionist(json o, EvalMethod method=EvalMethod::RECURSIVE)
Construct from a JSON object, to be mutated/copied by evaluate()/produce().
Definition expressionist.hpp:1050
void evaluate(json &object) const
Definition expressionist.hpp:1096
std::string tag() const
The currently configured expression tag.
Definition expressionist.hpp:1136
Expressionist(EvalMethod method=EvalMethod::RECURSIVE)
Definition expressionist.hpp:1072
Expressionist(std::string s, EvalMethod method=EvalMethod::RECURSIVE)
Definition expressionist.hpp:1055
json produce(json object) const
Definition expressionist.hpp:1116
std::string disableKey() const
The currently configured disable key.
Definition expressionist.hpp:1147
void setEvalMethod(EvalMethod method)
Definition expressionist.hpp:1125
void addConstant(const std::string &name, double value)
Definition expressionist.hpp:1154
const json & object() const
Access the (possibly evaluated) stored object.
Definition expressionist.hpp:1150
json produce() const
Definition expressionist.hpp:1107
Expressionist(const char *s, EvalMethod method=EvalMethod::RECURSIVE)
Same as the std::string overload; disambiguates string-literal calls.
Definition expressionist.hpp:1068
void evaluate()
Definition expressionist.hpp:1081
void setDisableKey(const std::string &key)
Definition expressionist.hpp:1145
void addBinaryFunction(const std::string &name, detail::BinaryFn fn)
Definition expressionist.hpp:1169