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 (and, optionally, toml++) string fields. See PURPOSE.md for
3// the design rationale.
4//
5// Copyright 2026 Paolo Bosetti
6// SPDX-License-Identifier: Apache-2.0
7//
8// A document string value that begins with the tag (default "$") is treated
9// as an algebraic expression whose variables are other keys of the same
10// document tree (a JSON object or a TOML table, selected at compile time via
11// EXPRESSIONIST_ENABLE_JSON / EXPRESSIONIST_ENABLE_TOML). The class resolves
12// inter-variable dependencies (order independent), detects circular
13// dependencies and undefined variables, and supports the usual arithmetic,
14// comparison and logical operators plus a library of mathematical functions
15// and constants.
16
17#pragma once
18
19// Backend selection: JSON is compiled in by default (opt-out), TOML is off by
20// default (opt-in). At least one backend must be enabled.
21#if !defined(EXPRESSIONIST_ENABLE_JSON)
22#define EXPRESSIONIST_ENABLE_JSON 1
23#endif
24#if !defined(EXPRESSIONIST_ENABLE_TOML)
25#define EXPRESSIONIST_ENABLE_TOML 0
26#endif
27#if !EXPRESSIONIST_ENABLE_JSON && !EXPRESSIONIST_ENABLE_TOML
28#error "Expressionist: enable at least one of EXPRESSIONIST_ENABLE_JSON / EXPRESSIONIST_ENABLE_TOML"
29#endif
30
31#include <algorithm>
32#include <cctype>
33#include <cmath>
34#include <cstdint>
35#include <deque>
36#include <functional>
37#include <map>
38#include <memory>
39#include <optional>
40#include <queue>
41#include <stdexcept>
42#include <string>
43#include <unordered_set>
44#include <variant>
45#include <vector>
46
47#if EXPRESSIONIST_ENABLE_JSON
48#include <nlohmann/json.hpp>
49#endif
50#if EXPRESSIONIST_ENABLE_TOML
51#include <toml++/toml.hpp>
52#endif
53
54namespace Expressionist {
55
56#if EXPRESSIONIST_ENABLE_JSON
57using json = nlohmann::json;
58#endif
59
63enum class EvalMethod {
64 GRAPH,
66 RECURSIVE,
68};
69
74class ExpressionistException : public std::exception {
75public:
76 explicit ExpressionistException(std::string message)
77 : _message(std::move(message)) {}
78 const char *what() const noexcept override { return _message.c_str(); }
79
80private:
81 std::string _message;
82}; // class ExpressionistException
83
84namespace detail {
85
86// A computed value is an integer, a double, a boolean or a sequence. Preserving
87// the integer/double distinction lets "$a + b" stay integral while "$pi/3" is a
88// float, matching the behaviour documented in PURPOSE.md. A sequence (produced
89// by the range operator "start:stop[:step]") is carried as a homogeneous
90// vector of integers or doubles, independent of any document format: it may
91// be written into a field but not used in scalar arithmetic. Each
92// DocumentModel (see below) materialises it into its own native array type.
93using Seq = std::variant<std::vector<std::int64_t>, std::vector<double>>;
94using Value = std::variant<std::int64_t, double, bool, Seq>;
95
96inline bool is_int(const Value &v) {
97 return std::holds_alternative<std::int64_t>(v);
98}
99
100inline bool is_bool_value(const Value &v) {
101 return std::holds_alternative<bool>(v);
102}
103
104inline bool is_seq(const Value &v) { return std::holds_alternative<Seq>(v); }
105
106inline double to_double(const Value &v) {
107 if (std::holds_alternative<std::int64_t>(v))
108 return static_cast<double>(std::get<std::int64_t>(v));
109 if (std::holds_alternative<double>(v))
110 return std::get<double>(v);
111 if (std::holds_alternative<bool>(v))
112 return std::get<bool>(v) ? 1.0 : 0.0;
113 throw ExpressionistException("a sequence cannot be used as a number");
114}
115
116inline bool to_bool(const Value &v) {
117 if (std::holds_alternative<bool>(v))
118 return std::get<bool>(v);
119 if (std::holds_alternative<std::int64_t>(v))
120 return std::get<std::int64_t>(v) != 0;
121 if (std::holds_alternative<double>(v))
122 return std::get<double>(v) != 0.0;
123 throw ExpressionistException("a sequence cannot be used as a boolean");
124}
125
126// ---------------------------------------------------------------------------
127// Symbol tables: constants and mathematical functions.
128// ---------------------------------------------------------------------------
129
130using UnaryFn = std::function<double(double)>;
131using BinaryFn = std::function<double(double, double)>;
132
133struct Symbols {
134 std::map<std::string, double> constants;
135 std::map<std::string, UnaryFn> unary;
136 std::map<std::string, BinaryFn> binary;
137}; // struct Symbols
138
139inline Symbols default_symbols() {
140 Symbols s;
141 // Literals are used instead of M_PI/M_E for portability (MSVC does not define
142 // them without _USE_MATH_DEFINES).
143 s.constants["pi"] = 3.14159265358979323846;
144 s.constants["e"] = 2.71828182845904523536;
145 s.constants["tau"] = 6.28318530717958647692;
146
147 s.unary["sin"] = [](double x) { return std::sin(x); };
148 s.unary["cos"] = [](double x) { return std::cos(x); };
149 s.unary["tan"] = [](double x) { return std::tan(x); };
150 s.unary["asin"] = [](double x) { return std::asin(x); };
151 s.unary["acos"] = [](double x) { return std::acos(x); };
152 s.unary["atan"] = [](double x) { return std::atan(x); };
153 s.unary["sinh"] = [](double x) { return std::sinh(x); };
154 s.unary["cosh"] = [](double x) { return std::cosh(x); };
155 s.unary["tanh"] = [](double x) { return std::tanh(x); };
156 s.unary["exp"] = [](double x) { return std::exp(x); };
157 s.unary["log"] = [](double x) { return std::log(x); }; // natural log
158 s.unary["ln"] = [](double x) { return std::log(x); };
159 s.unary["log10"] = [](double x) { return std::log10(x); };
160 s.unary["log2"] = [](double x) { return std::log2(x); };
161 s.unary["sqrt"] = [](double x) { return std::sqrt(x); };
162 s.unary["cbrt"] = [](double x) { return std::cbrt(x); };
163 s.unary["abs"] = [](double x) { return std::fabs(x); };
164 s.unary["floor"] = [](double x) { return std::floor(x); };
165 s.unary["ceil"] = [](double x) { return std::ceil(x); };
166 s.unary["round"] = [](double x) { return std::round(x); };
167 s.unary["trunc"] = [](double x) { return std::trunc(x); };
168 s.unary["sign"] = [](double x) {
169 return x > 0.0 ? 1.0 : (x < 0.0 ? -1.0 : 0.0);
170 };
171
172 s.binary["pow"] = [](double a, double b) { return std::pow(a, b); };
173 s.binary["atan2"] = [](double a, double b) { return std::atan2(a, b); };
174 s.binary["hypot"] = [](double a, double b) { return std::hypot(a, b); };
175 s.binary["min"] = [](double a, double b) { return std::min(a, b); };
176 s.binary["max"] = [](double a, double b) { return std::max(a, b); };
177 s.binary["mod"] = [](double a, double b) { return std::fmod(a, b); };
178 return s;
179}
180
181// ---------------------------------------------------------------------------
182// Tokenizer
183// ---------------------------------------------------------------------------
184
185enum class TokType {
186 Number,
187 Ident,
188 Plus,
189 Minus,
190 Star,
191 Slash,
192 Caret,
193 LParen,
194 RParen,
195 Comma,
196 Lt,
197 Le,
198 Gt,
199 Ge,
200 EqEq,
201 NotEq,
202 And,
203 Or,
204 Not,
205 Colon,
206 End
207}; // enum class TokType
208
209struct Token {
210 TokType type;
211 std::string text;
212 Value value; // valid when type == Number
213 std::size_t pos; // position in the source expression
214}; // struct Token
215
216class Tokenizer {
217public:
218 explicit Tokenizer(std::string src) : _src(std::move(src)) {}
219
220 std::vector<Token> tokenize() {
221 std::vector<Token> tokens;
222 for (;;) {
223 skip_ws();
224 if (_i >= _src.size()) {
225 tokens.push_back({TokType::End, "", Value{}, _i});
226 break;
227 }
228 char c = _src[_i];
229 if (is_digit(c) ||
230 (c == '.' && _i + 1 < _src.size() && is_digit(_src[_i + 1])))
231 tokens.push_back(read_number());
232 else if (std::isalpha(static_cast<unsigned char>(c)) || c == '_')
233 tokens.push_back(read_ident());
234 else
235 tokens.push_back(read_op());
236 }
237 return tokens;
238 }
239
240private:
241 static bool is_digit(char c) {
242 return std::isdigit(static_cast<unsigned char>(c)) != 0;
243 }
244
245 void skip_ws() {
246 while (_i < _src.size() &&
247 std::isspace(static_cast<unsigned char>(_src[_i])))
248 ++_i;
249 }
250
251 Token read_number() {
252 std::size_t start = _i;
253 bool is_float = false;
254 while (_i < _src.size() && is_digit(_src[_i]))
255 ++_i;
256 if (_i < _src.size() && _src[_i] == '.') {
257 is_float = true;
258 ++_i;
259 while (_i < _src.size() && is_digit(_src[_i]))
260 ++_i;
261 }
262 if (_i < _src.size() && (_src[_i] == 'e' || _src[_i] == 'E')) {
263 is_float = true;
264 ++_i;
265 if (_i < _src.size() && (_src[_i] == '+' || _src[_i] == '-'))
266 ++_i;
267 while (_i < _src.size() && is_digit(_src[_i]))
268 ++_i;
269 }
270 std::string text = _src.substr(start, _i - start);
271 Value v;
272 if (is_float) {
273 v = std::stod(text);
274 } else {
275 try {
276 v = static_cast<std::int64_t>(std::stoll(text));
277 } catch (const std::exception &) {
278 v = std::stod(text); // out of int64 range -> keep as double
279 }
280 }
281 return {TokType::Number, text, v, start};
282 }
283
284 Token read_ident() {
285 std::size_t start = _i;
286 while (
287 _i < _src.size() &&
288 (std::isalnum(static_cast<unsigned char>(_src[_i])) || _src[_i] == '_'))
289 ++_i;
290 return {TokType::Ident, _src.substr(start, _i - start), Value{}, start};
291 }
292
293 Token read_op() {
294 std::size_t start = _i;
295 char c = _src[_i];
296 auto two = [&](char a, char b) {
297 return _i + 1 < _src.size() && _src[_i] == a && _src[_i + 1] == b;
298 };
299 if (two('<', '='))
300 return _i += 2, Token{TokType::Le, "<=", {}, start};
301 if (two('>', '='))
302 return _i += 2, Token{TokType::Ge, ">=", {}, start};
303 if (two('=', '='))
304 return _i += 2, Token{TokType::EqEq, "==", {}, start};
305 if (two('!', '='))
306 return _i += 2, Token{TokType::NotEq, "!=", {}, start};
307 if (two('&', '&'))
308 return _i += 2, Token{TokType::And, "&&", {}, start};
309 if (two('|', '|'))
310 return _i += 2, Token{TokType::Or, "||", {}, start};
311 switch (c) {
312 case '+':
313 return ++_i, Token{TokType::Plus, "+", {}, start};
314 case '-':
315 return ++_i, Token{TokType::Minus, "-", {}, start};
316 case '*':
317 return ++_i, Token{TokType::Star, "*", {}, start};
318 case '/':
319 return ++_i, Token{TokType::Slash, "/", {}, start};
320 case '^':
321 return ++_i, Token{TokType::Caret, "^", {}, start};
322 case '(':
323 return ++_i, Token{TokType::LParen, "(", {}, start};
324 case ')':
325 return ++_i, Token{TokType::RParen, ")", {}, start};
326 case ',':
327 return ++_i, Token{TokType::Comma, ",", {}, start};
328 case ':':
329 return ++_i, Token{TokType::Colon, ":", {}, start};
330 case '<':
331 return ++_i, Token{TokType::Lt, "<", {}, start};
332 case '>':
333 return ++_i, Token{TokType::Gt, ">", {}, start};
334 case '!':
335 return ++_i, Token{TokType::Not, "!", {}, start};
336 default:
337 throw ExpressionistException("Unexpected character '" +
338 std::string(1, c) + "' at position " +
339 std::to_string(start));
340 }
341 }
342
343 std::string _src;
344 std::size_t _i = 0;
345}; // class Tokenizer
346
347// ---------------------------------------------------------------------------
348// Abstract syntax tree
349// ---------------------------------------------------------------------------
350
351enum class NodeKind {
352 IntLit,
353 FloatLit,
354 BoolLit,
355 Ident,
356 Unary,
357 Binary,
358 Call,
359 Range
360};
361
362struct Node {
363 NodeKind kind;
364 Value value; // literals
365 std::string name; // Ident / Call name
366 TokType op = TokType::End; // Unary / Binary operator
367 std::vector<std::shared_ptr<Node>> children; // operands / call arguments
368}; // struct Node
369
370using NodePtr = std::shared_ptr<Node>;
371
372class Parser {
373public:
374 Parser(std::vector<Token> tokens, std::string src)
375 : _tokens(std::move(tokens)), _src(std::move(src)) {}
376
377 NodePtr parse() {
378 NodePtr n = parse_range();
379 if (!check(TokType::End))
380 throw error("unexpected trailing token");
381 return n;
382 }
383
384private:
385 const Token &peek() const { return _tokens[_pos]; }
386 const Token &advance() { return _tokens[_pos++]; }
387 bool check(TokType t) const { return peek().type == t; }
388 bool match(TokType t) {
389 if (check(t)) {
390 ++_pos;
391 return true;
392 }
393 return false;
394 }
395
396 ExpressionistException error(const std::string &why) const {
397 return ExpressionistException("Parse error in \"" + _src + "\": " + why +
398 " ('" + peek().text + "' at position " +
399 std::to_string(peek().pos) + ")");
400 }
401
402 static NodePtr make_binary(TokType op, NodePtr l, NodePtr r) {
403 auto n = std::make_shared<Node>();
404 n->kind = NodeKind::Binary;
405 n->op = op;
406 n->children = {std::move(l), std::move(r)};
407 return n;
408 }
409
410 // A range is the lowest-precedence construct and appears only at the top of
411 // an expression: "start:stop" (integer bounds, implicit step 1) or
412 // "start:stop:step" (floating). Its parts are ordinary scalar expressions.
413 NodePtr parse_range() {
414 NodePtr first = parse_or();
415 if (!check(TokType::Colon))
416 return first;
417 auto node = std::make_shared<Node>();
418 node->kind = NodeKind::Range;
419 node->children.push_back(first);
420 while (match(TokType::Colon))
421 node->children.push_back(parse_or());
422 if (node->children.size() > 3)
423 throw error("a range has at most three parts (start:stop:step)");
424 return node;
425 }
426
427 NodePtr parse_or() {
428 NodePtr n = parse_and();
429 while (check(TokType::Or)) {
430 advance();
431 n = make_binary(TokType::Or, n, parse_and());
432 }
433 return n;
434 }
435
436 NodePtr parse_and() {
437 NodePtr n = parse_equality();
438 while (check(TokType::And)) {
439 advance();
440 n = make_binary(TokType::And, n, parse_equality());
441 }
442 return n;
443 }
444
445 NodePtr parse_equality() {
446 NodePtr n = parse_comparison();
447 while (check(TokType::EqEq) || check(TokType::NotEq)) {
448 TokType op = advance().type;
449 n = make_binary(op, n, parse_comparison());
450 }
451 return n;
452 }
453
454 NodePtr parse_comparison() {
455 NodePtr n = parse_additive();
456 while (check(TokType::Lt) || check(TokType::Le) || check(TokType::Gt) ||
457 check(TokType::Ge)) {
458 TokType op = advance().type;
459 n = make_binary(op, n, parse_additive());
460 }
461 return n;
462 }
463
464 NodePtr parse_additive() {
465 NodePtr n = parse_multiplicative();
466 while (check(TokType::Plus) || check(TokType::Minus)) {
467 TokType op = advance().type;
468 n = make_binary(op, n, parse_multiplicative());
469 }
470 return n;
471 }
472
473 NodePtr parse_multiplicative() {
474 NodePtr n = parse_unary();
475 while (check(TokType::Star) || check(TokType::Slash)) {
476 TokType op = advance().type;
477 n = make_binary(op, n, parse_unary());
478 }
479 return n;
480 }
481
482 NodePtr parse_unary() {
483 if (check(TokType::Minus) || check(TokType::Not)) {
484 TokType op = advance().type;
485 auto n = std::make_shared<Node>();
486 n->kind = NodeKind::Unary;
487 n->op = op;
488 n->children = {parse_unary()};
489 return n;
490 }
491 return parse_power();
492 }
493
494 // Power binds tighter than unary minus (so -2^2 == -(2^2)) and is
495 // right-associative (2^3^2 == 2^(3^2)); its exponent may itself be unary.
496 NodePtr parse_power() {
497 NodePtr base = parse_primary();
498 if (check(TokType::Caret)) {
499 advance();
500 return make_binary(TokType::Caret, base, parse_unary());
501 }
502 return base;
503 }
504
505 NodePtr parse_primary() {
506 const Token &t = peek();
507 if (t.type == TokType::Number) {
508 advance();
509 auto n = std::make_shared<Node>();
510 n->kind = is_int(t.value) ? NodeKind::IntLit : NodeKind::FloatLit;
511 n->value = t.value;
512 return n;
513 }
514 if (t.type == TokType::LParen) {
515 advance();
516 NodePtr n = parse_or();
517 if (!match(TokType::RParen))
518 throw error("expected ')'");
519 return n;
520 }
521 if (t.type == TokType::Ident) {
522 advance();
523 if (t.text == "true" || t.text == "false") {
524 auto n = std::make_shared<Node>();
525 n->kind = NodeKind::BoolLit;
526 n->value = (t.text == "true");
527 return n;
528 }
529 if (check(TokType::LParen)) {
530 advance();
531 auto n = std::make_shared<Node>();
532 n->kind = NodeKind::Call;
533 n->name = t.text;
534 if (!check(TokType::RParen)) {
535 n->children.push_back(parse_or());
536 while (match(TokType::Comma))
537 n->children.push_back(parse_or());
538 }
539 if (!match(TokType::RParen))
540 throw error("expected ')' in call to '" + t.text + "'");
541 return n;
542 }
543 auto n = std::make_shared<Node>();
544 n->kind = NodeKind::Ident;
545 n->name = t.text;
546 return n;
547 }
548 throw error("unexpected token");
549 }
550
551 std::vector<Token> _tokens;
552 std::string _src;
553 std::size_t _pos = 0;
554}; // class Parser
555
556// Names appearing in variable position (constants included; the caller decides
557// which are keys vs. constants). Function names are not collected.
558inline void collect_idents(const NodePtr &n, std::vector<std::string> &out) {
559 if (!n)
560 return;
561 if (n->kind == NodeKind::Ident)
562 out.push_back(n->name);
563 for (const auto &c : n->children)
564 collect_idents(c, out);
565}
566
567using Resolver = std::function<std::optional<Value>(const std::string &)>;
568
569inline Value num_binary(const Value &l, const Value &r, TokType op) {
570 if (op == TokType::Slash)
571 return to_double(l) / to_double(r); // division is always floating point
572 if (op == TokType::Caret) {
573 if (is_int(l) && is_int(r) && std::get<std::int64_t>(r) >= 0) {
574 std::int64_t base = std::get<std::int64_t>(l);
575 std::int64_t exp = std::get<std::int64_t>(r);
576 std::int64_t result = 1;
577 for (std::int64_t k = 0; k < exp; ++k)
578 result *= base;
579 return result;
580 }
581 return std::pow(to_double(l), to_double(r));
582 }
583 if (is_int(l) && is_int(r)) {
584 std::int64_t a = std::get<std::int64_t>(l);
585 std::int64_t b = std::get<std::int64_t>(r);
586 switch (op) {
587 case TokType::Plus:
588 return a + b;
589 case TokType::Minus:
590 return a - b;
591 case TokType::Star:
592 return a * b;
593 default:
594 break;
595 }
596 }
597 double a = to_double(l);
598 double b = to_double(r);
599 switch (op) {
600 case TokType::Plus:
601 return a + b;
602 case TokType::Minus:
603 return a - b;
604 case TokType::Star:
605 return a * b;
606 default:
607 return 0.0; // unreachable
608 }
609}
610
611inline Value eval_node(const NodePtr &n, const Symbols &sym,
612 const Resolver &resolve) {
613 switch (n->kind) {
614 case NodeKind::IntLit:
615 case NodeKind::FloatLit:
616 case NodeKind::BoolLit:
617 return n->value;
618 case NodeKind::Ident: {
619 if (auto v = resolve(n->name)) // a key shadows a same-named constant
620 return *v;
621 auto it = sym.constants.find(n->name);
622 if (it != sym.constants.end())
623 return it->second;
624 throw ExpressionistException("Undefined variable or constant: '" + n->name +
625 "'");
626 }
627 case NodeKind::Unary: {
628 if (n->op == TokType::Not)
629 return !to_bool(eval_node(n->children[0], sym, resolve));
630 Value c = eval_node(n->children[0], sym, resolve);
631 if (is_int(c))
632 return -std::get<std::int64_t>(c);
633 return -to_double(c);
634 }
635 case NodeKind::Binary: {
636 if (n->op == TokType::And) {
637 if (!to_bool(eval_node(n->children[0], sym, resolve)))
638 return false;
639 return to_bool(eval_node(n->children[1], sym, resolve));
640 }
641 if (n->op == TokType::Or) {
642 if (to_bool(eval_node(n->children[0], sym, resolve)))
643 return true;
644 return to_bool(eval_node(n->children[1], sym, resolve));
645 }
646 Value l = eval_node(n->children[0], sym, resolve);
647 Value r = eval_node(n->children[1], sym, resolve);
648 switch (n->op) {
649 case TokType::Plus:
650 case TokType::Minus:
651 case TokType::Star:
652 case TokType::Slash:
653 case TokType::Caret:
654 return num_binary(l, r, n->op);
655 case TokType::Lt:
656 return to_double(l) < to_double(r);
657 case TokType::Le:
658 return to_double(l) <= to_double(r);
659 case TokType::Gt:
660 return to_double(l) > to_double(r);
661 case TokType::Ge:
662 return to_double(l) >= to_double(r);
663 case TokType::EqEq:
664 if (is_bool_value(l) && is_bool_value(r))
665 return std::get<bool>(l) == std::get<bool>(r);
666 return to_double(l) == to_double(r);
667 case TokType::NotEq:
668 if (is_bool_value(l) && is_bool_value(r))
669 return std::get<bool>(l) != std::get<bool>(r);
670 return to_double(l) != to_double(r);
671 default:
672 throw ExpressionistException("Internal error: bad binary operator");
673 }
674 }
675 case NodeKind::Range: {
676 if (n->children.size() == 2) {
677 Value a = eval_node(n->children[0], sym, resolve);
678 Value b = eval_node(n->children[1], sym, resolve);
679 if (!is_int(a) || !is_int(b))
680 throw ExpressionistException(
681 "Binary range 'start:stop' requires integer bounds");
682 std::int64_t start = std::get<std::int64_t>(a);
683 std::int64_t stop = std::get<std::int64_t>(b);
684 std::vector<std::int64_t> arr;
685 for (std::int64_t v = start; v <= stop; ++v)
686 arr.push_back(v);
687 return Value(Seq(std::move(arr)));
688 }
689 if (n->children.size() == 3) {
690 double start = to_double(eval_node(n->children[0], sym, resolve));
691 double stop = to_double(eval_node(n->children[1], sym, resolve));
692 double step = to_double(eval_node(n->children[2], sym, resolve));
693 if (step == 0.0)
694 throw ExpressionistException("Range step must be non-zero");
695 std::vector<double> arr;
696 double span = stop - start;
697 // Generate only when the step points from start towards stop; a step in
698 // the wrong direction yields an empty sequence rather than diverging. The
699 // small epsilon keeps the endpoint inclusive despite rounding, so
700 // "0:1:0.1" ends exactly at 1.
701 if (span == 0.0 || (span > 0.0) == (step > 0.0)) {
702 std::int64_t count =
703 static_cast<std::int64_t>(std::floor(span / step + 1e-9));
704 for (std::int64_t i = 0; i <= count; ++i)
705 arr.push_back(start + static_cast<double>(i) * step);
706 }
707 return Value(Seq(std::move(arr)));
708 }
709 throw ExpressionistException(
710 "A range must have two (start:stop) or three (start:stop:step) parts");
711 }
712 case NodeKind::Call: {
713 const std::string &fn = n->name;
714 auto u = sym.unary.find(fn);
715 if (u != sym.unary.end()) {
716 if (n->children.size() != 1)
717 throw ExpressionistException("Function '" + fn +
718 "' expects 1 argument, got " +
719 std::to_string(n->children.size()));
720 return u->second(to_double(eval_node(n->children[0], sym, resolve)));
721 }
722 auto b = sym.binary.find(fn);
723 if (b != sym.binary.end()) {
724 if (n->children.size() != 2)
725 throw ExpressionistException("Function '" + fn +
726 "' expects 2 arguments, got " +
727 std::to_string(n->children.size()));
728 double x = to_double(eval_node(n->children[0], sym, resolve));
729 double y = to_double(eval_node(n->children[1], sym, resolve));
730 return b->second(x, y);
731 }
732 throw ExpressionistException("Unknown function: '" + fn + "'");
733 }
734 }
735 throw ExpressionistException("Internal error: bad node");
736}
737
738// ---------------------------------------------------------------------------
739// DocumentModel: abstracts the tree the Evaluator walks, so the engine above
740// this line has no dependency on any particular document format. A NodeRef is
741// an opaque, model-owned handle to a node; only the owning model may
742// dereference it. Two concrete models follow: JsonModel (nlohmann::json) and
743// TomlModel (toml++). The Evaluator only ever pulls through this interface --
744// it never touches nlohmann::json or toml++ directly.
745// ---------------------------------------------------------------------------
746
747struct NodeRef {
748 void *p = nullptr;
749};
750
751class DocumentModel {
752public:
753 virtual ~DocumentModel() = default;
754 virtual NodeRef root() = 0;
755 virtual bool is_object(NodeRef node) const = 0;
756 virtual bool is_array(NodeRef node) const = 0;
757 virtual void
758 for_each_member(NodeRef node,
759 const std::function<void(const std::string &, NodeRef)> &fn) = 0;
760 virtual void
761 for_each_element(NodeRef node,
762 const std::function<void(std::size_t, NodeRef)> &fn) = 0;
763 // The node's string content, for the tag-prefix check; nullopt if the node
764 // is not a string.
765 virtual std::optional<std::string> as_string(NodeRef node) const = 0;
766 // The node's value as a numeric/boolean literal; nullopt if the node holds
767 // something else (a string, a sequence, a nested container, or a
768 // format-specific scalar such as a TOML date/time).
769 virtual std::optional<Value> as_scalar(NodeRef node) const = 0;
770 // Overwrites `node` with the computed value, in place.
771 virtual void assign(NodeRef node, const Value &v) = 0;
772 // True iff `object` has a member named `key` whose literal value is the
773 // boolean `false` -- backs the subtree disable-key opt-out.
774 virtual bool has_false_member(NodeRef object, const std::string &key) const = 0;
775}; // class DocumentModel
776
777#if EXPRESSIONIST_ENABLE_JSON
778
779class JsonModel : public DocumentModel {
780public:
781 explicit JsonModel(json &root) : _root(root) {}
782
783 NodeRef root() override { return NodeRef{&_root}; }
784
785 bool is_object(NodeRef node) const override {
786 return as_json(node).is_object();
787 }
788 bool is_array(NodeRef node) const override {
789 return as_json(node).is_array();
790 }
791
792 void for_each_member(
793 NodeRef node,
794 const std::function<void(const std::string &, NodeRef)> &fn) override {
795 json &obj = as_json(node);
796 for (auto it = obj.begin(); it != obj.end(); ++it)
797 fn(it.key(), NodeRef{&it.value()});
798 }
799
800 void for_each_element(
801 NodeRef node,
802 const std::function<void(std::size_t, NodeRef)> &fn) override {
803 json &arr = as_json(node);
804 for (std::size_t i = 0; i < arr.size(); ++i)
805 fn(i, NodeRef{&arr[i]});
806 }
807
808 std::optional<std::string> as_string(NodeRef node) const override {
809 const json &v = as_json(node);
810 if (!v.is_string())
811 return std::nullopt;
812 return v.get<std::string>();
813 }
814
815 std::optional<Value> as_scalar(NodeRef node) const override {
816 const json &v = as_json(node);
817 if (v.is_number_integer())
818 return Value(static_cast<std::int64_t>(v.get<std::int64_t>()));
819 if (v.is_number_float())
820 return Value(v.get<double>());
821 if (v.is_boolean())
822 return Value(v.get<bool>());
823 return std::nullopt;
824 }
825
826 void assign(NodeRef node, const Value &v) override {
827 json &slot = as_json(node);
828 if (std::holds_alternative<std::int64_t>(v)) {
829 slot = json(std::get<std::int64_t>(v));
830 } else if (std::holds_alternative<double>(v)) {
831 slot = json(std::get<double>(v));
832 } else if (std::holds_alternative<bool>(v)) {
833 slot = json(std::get<bool>(v));
834 } else {
835 const Seq &seq = std::get<Seq>(v);
836 json arr = json::array();
837 if (std::holds_alternative<std::vector<std::int64_t>>(seq)) {
838 for (std::int64_t x : std::get<std::vector<std::int64_t>>(seq))
839 arr.push_back(x);
840 } else {
841 for (double x : std::get<std::vector<double>>(seq))
842 arr.push_back(x);
843 }
844 slot = std::move(arr);
845 }
846 }
847
848 bool has_false_member(NodeRef object, const std::string &key) const override {
849 const json &obj = as_json(object);
850 auto it = obj.find(key);
851 return it != obj.end() && *it == false;
852 }
853
854private:
855 static json &as_json(NodeRef node) { return *static_cast<json *>(node.p); }
856
857 json &_root;
858}; // class JsonModel
859
860#endif // EXPRESSIONIST_ENABLE_JSON
861
862#if EXPRESSIONIST_ENABLE_TOML
863
864class TomlModel : public DocumentModel {
865public:
866 explicit TomlModel(toml::table &root) {
867 _slots.push_back(Slot{&root, nullptr, std::string(), 0, false});
868 }
869
870 NodeRef root() override { return NodeRef{&_slots.front()}; }
871
872 bool is_object(NodeRef node) const override {
873 return slot(node).self->is_table();
874 }
875 bool is_array(NodeRef node) const override {
876 return slot(node).self->is_array();
877 }
878
879 void for_each_member(
880 NodeRef node,
881 const std::function<void(const std::string &, NodeRef)> &fn) override {
882 toml::table *t = slot(node).self->as_table();
883 for (auto &&[key, val] : *t) {
884 std::string name(key.str());
885 _slots.push_back(Slot{&val, slot(node).self, name, 0, false});
886 fn(name, NodeRef{&_slots.back()});
887 }
888 }
889
890 void for_each_element(
891 NodeRef node,
892 const std::function<void(std::size_t, NodeRef)> &fn) override {
893 toml::array *a = slot(node).self->as_array();
894 std::size_t i = 0;
895 for (auto &&val : *a) {
896 _slots.push_back(Slot{&val, slot(node).self, std::string(), i, true});
897 fn(i, NodeRef{&_slots.back()});
898 ++i;
899 }
900 }
901
902 std::optional<std::string> as_string(NodeRef node) const override {
903 if (auto *v = slot(node).self->as_string())
904 return v->get();
905 return std::nullopt;
906 }
907
908 std::optional<Value> as_scalar(NodeRef node) const override {
909 const toml::node *n = slot(node).self;
910 if (auto *v = n->as_integer())
911 return Value(v->get());
912 if (auto *v = n->as_floating_point())
913 return Value(v->get());
914 if (auto *v = n->as_boolean())
915 return Value(v->get());
916 return std::nullopt; // string, date, time, date_time, table, array
917 }
918
919 void assign(NodeRef node, const Value &v) override {
920 Slot &s = slot(node);
921 if (s.parent_is_array) {
922 toml::array *arr = s.parent->as_array();
923 auto pos = arr->cbegin() + static_cast<std::ptrdiff_t>(s.index);
924 with_native(v, [&](auto &&native) {
925 auto it = arr->replace(pos, std::forward<decltype(native)>(native));
926 s.self = &*it;
927 });
928 } else {
929 toml::table *tbl = s.parent->as_table();
930 with_native(v, [&](auto &&native) {
931 tbl->insert_or_assign(s.key, std::forward<decltype(native)>(native));
932 });
933 s.self = tbl->get(s.key);
934 }
935 }
936
937 bool has_false_member(NodeRef object, const std::string &key) const override {
938 const toml::table *t = slot(object).self->as_table();
939 if (!t)
940 return false;
941 const toml::node *member = t->get(key);
942 if (!member)
943 return false;
944 const auto *b = member->as_boolean();
945 return b && b->get() == false;
946 }
947
948private:
949 // A bare toml::node* cannot change its own type in place, so write-back
950 // must go through the parent (table::insert_or_assign / array::replace).
951 // Each Slot remembers that parent plus the key/index needed to do so.
952 struct Slot {
953 toml::node *self;
954 toml::node *parent; // nullptr only for the root
955 std::string key; // valid when parent is a table
956 std::size_t index; // valid when parent is an array
957 bool parent_is_array;
958 };
959
960 Slot &slot(NodeRef node) { return *static_cast<Slot *>(node.p); }
961 const Slot &slot(NodeRef node) const {
962 return *static_cast<const Slot *>(node.p);
963 }
964
965 template <typename Setter> static void with_native(const Value &v, Setter &&set) {
966 if (std::holds_alternative<std::int64_t>(v)) {
967 set(std::get<std::int64_t>(v));
968 } else if (std::holds_alternative<double>(v)) {
969 set(std::get<double>(v));
970 } else if (std::holds_alternative<bool>(v)) {
971 set(std::get<bool>(v));
972 } else {
973 const Seq &seq = std::get<Seq>(v);
974 toml::array arr;
975 if (std::holds_alternative<std::vector<std::int64_t>>(seq)) {
976 for (std::int64_t x : std::get<std::vector<std::int64_t>>(seq))
977 arr.push_back(x);
978 } else {
979 for (double x : std::get<std::vector<double>>(seq))
980 arr.push_back(x);
981 }
982 set(std::move(arr));
983 }
984 }
985
986 std::deque<Slot> _slots; // stable addresses for Slot*
987}; // class TomlModel
988
989#endif // EXPRESSIONIST_ENABLE_TOML
990
991// ---------------------------------------------------------------------------
992// Evaluator: walks a document tree (via DocumentModel), builds scopes/cells
993// and evaluates expressions.
994// ---------------------------------------------------------------------------
995
996class Evaluator {
997public:
998 Evaluator(const Symbols &sym, std::string tag, std::string disableKey,
999 EvalMethod method)
1000 : _sym(sym), _tag(std::move(tag)), _disableKey(std::move(disableKey)),
1001 _method(method) {}
1002
1003 void run(DocumentModel &doc) {
1004 _doc = &doc;
1005 _cells.clear();
1006 _scopes.clear();
1007 walk(doc.root(), nullptr, "");
1008 if (_method == EvalMethod::GRAPH)
1009 eval_graph();
1010 else
1011 eval_recursive();
1012 for (const Cell &c : _cells)
1013 if (c.is_expr) // all expression cells are evaluated on success
1014 _doc->assign(c.node, c.result);
1015 }
1016
1017private:
1018 struct Scope {
1019 Scope *parent = nullptr;
1020 std::map<std::string, std::size_t> keys; // key -> cell index
1021 };
1022
1023 struct Cell {
1024 NodeRef node; // location to overwrite with the computed value
1025 Scope *scope = nullptr;
1026 std::string name; // last path component, for diagnostics
1027 std::string path; // full path, for diagnostics
1028 bool is_expr = false;
1029 std::string source; // expression text (tag stripped)
1030 NodePtr ast;
1031 Value result;
1032 int state = 0; // 0 unvisited, 1 visiting, 2 done (recursive method)
1033 };
1034
1035 bool starts_with_tag(const std::string &s) const {
1036 return !_tag.empty() && s.rfind(_tag, 0) == 0;
1037 }
1038
1039 // An object opts its whole subtree out of evaluation by carrying a
1040 // `disableKey: false` member (default key: "expressionist"). The check
1041 // looks only at the literal document value, never at an evaluated result,
1042 // so it works uniformly regardless of definition order or evaluation
1043 // strategy.
1044 bool is_disabled(NodeRef object) const {
1045 return !_disableKey.empty() && _doc->has_false_member(object, _disableKey);
1046 }
1047
1048 std::size_t add_cell(NodeRef node, Scope *scope, const std::string &path) {
1049 std::size_t idx = _cells.size();
1050 _cells.emplace_back();
1051 Cell &cell = _cells.back();
1052 cell.node = node;
1053 cell.scope = scope;
1054 cell.path = path;
1055 cell.name = last_component(path);
1056 if (auto s = _doc->as_string(node)) {
1057 if (starts_with_tag(*s)) {
1058 cell.is_expr = true;
1059 cell.source = s->substr(_tag.size());
1060 try {
1061 Tokenizer tok(cell.source);
1062 Parser parser(tok.tokenize(), cell.source);
1063 cell.ast = parser.parse();
1064 } catch (const ExpressionistException &e) {
1065 throw ExpressionistException(cell_context(cell) + e.what());
1066 }
1067 }
1068 }
1069 return idx;
1070 }
1071
1072 void walk(NodeRef node, Scope *enclosing, const std::string &path) {
1073 if (_doc->is_object(node)) {
1074 if (is_disabled(node))
1075 return; // leave this object and everything under it untouched
1076 _scopes.emplace_back();
1077 Scope *scope = &_scopes.back();
1078 scope->parent = enclosing;
1079 std::vector<std::pair<std::string, NodeRef>> members;
1080 _doc->for_each_member(node, [&](const std::string &key, NodeRef child) {
1081 members.emplace_back(key, child);
1082 });
1083 // Register every member first so sibling references resolve regardless of
1084 // definition order.
1085 for (auto &[key, child] : members)
1086 scope->keys[key] = add_cell(child, scope, join(path, key));
1087 // Then descend into nested containers for their own scopes / inner cells.
1088 for (auto &[key, child] : members)
1089 if (_doc->is_object(child) || _doc->is_array(child))
1090 walk(child, scope, join(path, key));
1091 } else if (_doc->is_array(node)) {
1092 // Arrays are not scopes: their elements resolve in the enclosing object.
1093 std::vector<NodeRef> elements;
1094 _doc->for_each_element(
1095 node, [&](std::size_t, NodeRef child) { elements.push_back(child); });
1096 for (std::size_t i = 0; i < elements.size(); ++i) {
1097 std::string p = path + "/" + std::to_string(i);
1098 NodeRef child = elements[i];
1099 if (_doc->is_object(child) || _doc->is_array(child))
1100 walk(child, enclosing, p);
1101 else
1102 add_cell(child, enclosing, p);
1103 }
1104 }
1105 }
1106
1107 // Lexical lookup: nearest enclosing object scope, then ancestors.
1108 std::size_t resolve_name(const std::string &name, Scope *scope,
1109 bool &found) const {
1110 for (Scope *s = scope; s != nullptr; s = s->parent) {
1111 auto it = s->keys.find(name);
1112 if (it != s->keys.end()) {
1113 found = true;
1114 return it->second;
1115 }
1116 }
1117 found = false;
1118 return 0;
1119 }
1120
1121 Value literal_value(const Cell &c) const {
1122 if (auto v = _doc->as_scalar(c.node))
1123 return *v;
1124 throw ExpressionistException("variable '" + c.name + "' is not numeric");
1125 }
1126
1127 std::string cell_context(const Cell &c) const {
1128 return "In '" + c.path + "' (\"" + _tag + c.source + "\"): ";
1129 }
1130
1131 // Add the offending cell's location once, avoiding duplicate prefixes as the
1132 // exception unwinds through nested evaluations.
1133 [[noreturn]] void
1134 rethrow_with_context(const Cell &c, const ExpressionistException &e) const {
1135 std::string msg = e.what();
1136 if (msg.rfind("In '", 0) == 0)
1137 throw e;
1138 throw ExpressionistException(cell_context(c) + msg);
1139 }
1140
1141 // --- Recursive strategy -------------------------------------------------
1142
1143 void eval_recursive() {
1144 std::vector<std::size_t> stack;
1145 for (std::size_t i = 0; i < _cells.size(); ++i)
1146 if (_cells[i].is_expr)
1147 eval_cell(i, stack);
1148 }
1149
1150 Value eval_cell(std::size_t idx, std::vector<std::size_t> &stack) {
1151 Cell &cell = _cells[idx];
1152 if (!cell.is_expr)
1153 return literal_value(cell);
1154 if (cell.state == 2)
1155 return cell.result;
1156 if (cell.state == 1)
1157 throw ExpressionistException("Circular dependency: " +
1158 cycle_path(stack, idx));
1159 cell.state = 1;
1160 stack.push_back(idx);
1161 Scope *scope = cell.scope;
1162 Resolver resolver = [&](const std::string &name) -> std::optional<Value> {
1163 bool found;
1164 std::size_t tgt = resolve_name(name, scope, found);
1165 if (!found)
1166 return std::nullopt;
1167 return eval_cell(tgt, stack);
1168 };
1169 Value v;
1170 try {
1171 v = eval_node(cell.ast, _sym, resolver);
1172 } catch (const ExpressionistException &e) {
1173 rethrow_with_context(cell, e);
1174 }
1175 stack.pop_back();
1176 cell.result = v;
1177 cell.state = 2;
1178 return v;
1179 }
1180
1181 std::string cycle_path(const std::vector<std::size_t> &stack,
1182 std::size_t idx) const {
1183 std::size_t start = 0;
1184 for (std::size_t k = 0; k < stack.size(); ++k)
1185 if (stack[k] == idx) {
1186 start = k;
1187 break;
1188 }
1189 std::string m;
1190 for (std::size_t k = start; k < stack.size(); ++k)
1191 m += _cells[stack[k]].name + " -> ";
1192 m += _cells[idx].name;
1193 return m;
1194 }
1195
1196 // --- Graph strategy (topological order via Kahn's algorithm) ------------
1197
1198 void eval_graph() {
1199 std::size_t n = _cells.size();
1200 std::vector<std::vector<std::size_t>> dependents(n);
1201 std::vector<int> indeg(n, 0);
1202 std::vector<std::size_t> expr_cells;
1203 for (std::size_t i = 0; i < n; ++i) {
1204 if (!_cells[i].is_expr)
1205 continue;
1206 expr_cells.push_back(i);
1207 std::vector<std::string> idents;
1208 collect_idents(_cells[i].ast, idents);
1209 std::unordered_set<std::size_t> seen;
1210 for (const std::string &name : idents) {
1211 bool found;
1212 std::size_t tgt = resolve_name(name, _cells[i].scope, found);
1213 if (found && _cells[tgt].is_expr && seen.insert(tgt).second) {
1214 dependents[tgt].push_back(i);
1215 ++indeg[i];
1216 }
1217 }
1218 }
1219 std::queue<std::size_t> q;
1220 for (std::size_t i : expr_cells)
1221 if (indeg[i] == 0)
1222 q.push(i);
1223 std::size_t processed = 0;
1224 while (!q.empty()) {
1225 std::size_t i = q.front();
1226 q.pop();
1227 ++processed;
1228 eval_expr_graph(i);
1229 for (std::size_t d : dependents[i])
1230 if (--indeg[d] == 0)
1231 q.push(d);
1232 }
1233 if (processed != expr_cells.size()) {
1234 std::string names;
1235 for (std::size_t i : expr_cells)
1236 if (_cells[i].state != 2)
1237 names += _cells[i].name + " ";
1238 throw ExpressionistException("Circular dependency detected among: " +
1239 names);
1240 }
1241 }
1242
1243 void eval_expr_graph(std::size_t idx) {
1244 Cell &cell = _cells[idx];
1245 Scope *scope = cell.scope;
1246 Resolver resolver = [&](const std::string &name) -> std::optional<Value> {
1247 bool found;
1248 std::size_t tgt = resolve_name(name, scope, found);
1249 if (!found)
1250 return std::nullopt;
1251 const Cell &t = _cells[tgt];
1252 if (t.is_expr)
1253 return t.result; // already evaluated: dependencies come first
1254 return literal_value(t);
1255 };
1256 try {
1257 cell.result = eval_node(cell.ast, _sym, resolver);
1258 } catch (const ExpressionistException &e) {
1259 rethrow_with_context(cell, e);
1260 }
1261 cell.state = 2;
1262 }
1263
1264 static std::string join(const std::string &path, const std::string &key) {
1265 return path + "/" + key;
1266 }
1267
1268 static std::string last_component(const std::string &path) {
1269 std::size_t p = path.find_last_of('/');
1270 return p == std::string::npos ? path : path.substr(p + 1);
1271 }
1272
1273 const Symbols &_sym;
1274 std::string _tag;
1275 std::string _disableKey;
1276 EvalMethod _method;
1277 DocumentModel *_doc = nullptr;
1278 std::deque<Scope> _scopes; // stable addresses for Scope*
1279 std::vector<Cell> _cells;
1280}; // class Evaluator
1281
1282} // namespace detail
1283
1288
1321public:
1322#if EXPRESSIONIST_ENABLE_JSON
1324 Expressionist(json o, EvalMethod method = EvalMethod::RECURSIVE)
1325 : _object(std::move(o)), _evalMethod(method),
1326 _symbols(detail::default_symbols()) {}
1329 Expressionist(std::string s, EvalMethod method = EvalMethod::RECURSIVE)
1330 : _evalMethod(method), _symbols(detail::default_symbols()) {
1331 try {
1332 _object = json::parse(s);
1333 } catch (const std::exception &e) {
1334 throw ExpressionistException("Failed to parse JSON: " +
1335 std::string(e.what()));
1336 }
1337 }
1338 // A string literal is otherwise ambiguous between the json and std::string
1339 // constructors (each a single user-defined conversion from const char*).
1340 // This exact-match overload disambiguates in favour of JSON parsing.
1342 Expressionist(const char *s, EvalMethod method = EvalMethod::RECURSIVE)
1343 : Expressionist(std::string(s), method) {}
1344#endif // EXPRESSIONIST_ENABLE_JSON
1345#if EXPRESSIONIST_ENABLE_TOML
1348 explicit Expressionist(toml::table t, EvalMethod method = EvalMethod::RECURSIVE)
1349 : _toml(std::move(t)), _evalMethod(method),
1350 _symbols(detail::default_symbols()) {}
1353 static Expressionist parse_toml(const std::string &s,
1354 EvalMethod method = EvalMethod::RECURSIVE) {
1355 try {
1356 toml::table t = toml::parse(s);
1357 return Expressionist(std::move(t), method);
1358 } catch (const std::exception &e) {
1359 throw ExpressionistException("Failed to parse TOML: " +
1360 std::string(e.what()));
1361 }
1362 }
1363#endif // EXPRESSIONIST_ENABLE_TOML
1367 Expressionist(EvalMethod method = EvalMethod::RECURSIVE)
1368 : _evalMethod(method), _symbols(detail::default_symbols()) {}
1369
1370 ~Expressionist() = default;
1371
1372#if EXPRESSIONIST_ENABLE_JSON
1377 void evaluate() {
1378 detail::JsonModel model(_object);
1379 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1380 ev.run(model);
1381 }
1382
1393 void evaluate(json &object) const {
1394 detail::JsonModel model(object);
1395 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1396 ev.run(model);
1397 }
1398
1405 json produce() const {
1406 json copy = _object;
1407 detail::JsonModel model(copy);
1408 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1409 ev.run(model);
1410 return copy;
1411 }
1412
1415 json produce(json object) const {
1416 json copy = object;
1417 detail::JsonModel model(copy);
1418 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1419 ev.run(model);
1420 return copy;
1421 }
1422#endif // EXPRESSIONIST_ENABLE_JSON
1423
1424#if EXPRESSIONIST_ENABLE_TOML
1427 void evaluate(toml::table &object) const {
1428 detail::TomlModel model(object);
1429 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1430 ev.run(model);
1431 }
1432
1435 toml::table produce(toml::table object) const {
1436 toml::table copy = object;
1437 detail::TomlModel model(copy);
1438 detail::Evaluator ev(_symbols, _tag, _disableKey, _evalMethod);
1439 ev.run(model);
1440 return copy;
1441 }
1442#endif // EXPRESSIONIST_ENABLE_TOML
1443
1446 void setEvalMethod(EvalMethod method) { _evalMethod = method; }
1448 EvalMethod getEvalMethod() const { return _evalMethod; }
1449
1455 void setTag(const std::string &tag) { _tag = tag; }
1457 std::string tag() const { return _tag; }
1458
1466 void setDisableKey(const std::string &key) { _disableKey = key; }
1468 std::string disableKey() const { return _disableKey; }
1469
1470#if EXPRESSIONIST_ENABLE_JSON
1472 const json &object() const { return _object; }
1473#endif // EXPRESSIONIST_ENABLE_JSON
1474#if EXPRESSIONIST_ENABLE_TOML
1476 const toml::table &as_toml() const { return _toml; }
1477#endif // EXPRESSIONIST_ENABLE_TOML
1478
1481 void addConstant(const std::string &name, double value) {
1482 _symbols.constants[name] = value;
1483 }
1488 void addUnaryFunction(const std::string &name, detail::UnaryFn fn) {
1489 _symbols.unary[name] = std::move(fn);
1490 }
1496 void addBinaryFunction(const std::string &name, detail::BinaryFn fn) {
1497 _symbols.binary[name] = std::move(fn);
1498 }
1499
1500private:
1501#if EXPRESSIONIST_ENABLE_JSON
1502 json _object = json::object();
1503#endif // EXPRESSIONIST_ENABLE_JSON
1504#if EXPRESSIONIST_ENABLE_TOML
1505 toml::table _toml;
1506#endif // EXPRESSIONIST_ENABLE_TOML
1507 EvalMethod _evalMethod = EvalMethod::RECURSIVE;
1508 std::string _tag = "$";
1509 std::string _disableKey = "expressionist";
1510 detail::Symbols _symbols;
1511}; // class Expressionist
1512
1513} // namespace Expressionist
Definition expressionist.hpp:74
Definition expressionist.hpp:1320
void setTag(const std::string &tag)
Definition expressionist.hpp:1455
void addUnaryFunction(const std::string &name, detail::UnaryFn fn)
Definition expressionist.hpp:1488
EvalMethod getEvalMethod() const
The currently selected resolution strategy.
Definition expressionist.hpp:1448
Expressionist(json o, EvalMethod method=EvalMethod::RECURSIVE)
Construct from a JSON object, to be mutated/copied by evaluate()/produce().
Definition expressionist.hpp:1324
void evaluate(json &object) const
Definition expressionist.hpp:1393
std::string tag() const
The currently configured expression tag.
Definition expressionist.hpp:1457
Expressionist(std::string s, EvalMethod method=EvalMethod::RECURSIVE)
Definition expressionist.hpp:1329
json produce(json object) const
Definition expressionist.hpp:1415
std::string disableKey() const
The currently configured disable key.
Definition expressionist.hpp:1468
void setEvalMethod(EvalMethod method)
Definition expressionist.hpp:1446
void addConstant(const std::string &name, double value)
Definition expressionist.hpp:1481
const json & object() const
Access the (possibly evaluated) stored object.
Definition expressionist.hpp:1472
json produce() const
Definition expressionist.hpp:1405
Expressionist(const char *s, EvalMethod method=EvalMethod::RECURSIVE)
Same as the std::string overload; disambiguates string-literal calls.
Definition expressionist.hpp:1342
void evaluate()
Definition expressionist.hpp:1377
void setDisableKey(const std::string &key)
Definition expressionist.hpp:1466
void addBinaryFunction(const std::string &name, detail::BinaryFn fn)
Definition expressionist.hpp:1496