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