// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Debug:Condition_impl; import std; import Crafter.Debug.Core; import :Condition; using namespace Crafter::Debug; struct Condition::Node { enum class Op : std::uint8_t { Or, And, Not, Compare, Truthy, }; enum class Comparison : std::uint8_t { Equal, NotEqual, Less, LessEqual, Greater, GreaterEqual, }; struct Operand { // A path to resolve, or else a literal. std::string path; Value literal; }; Op op = Op::Truthy; Comparison comparison = Comparison::Equal; std::shared_ptr left; std::shared_ptr right; Operand a; Operand b; // The text this node was parsed from, for errors. std::string source; }; namespace { using Node = Condition::Node; using Outcome = std::expected; // Nesting deeper than this is refused rather than recursed into. constexpr std::uint32_t maxDepth = 64; bool PathStart(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } bool PathChar(char c) { return PathStart(c) || (c >= '0' && c <= '9') || c == '-'; } std::string_view ComparisonText(Node::Comparison comparison) { switch (comparison) { case Node::Comparison::Equal: return "=="; case Node::Comparison::NotEqual: return "!="; case Node::Comparison::Less: return "<"; case Node::Comparison::LessEqual: return "<="; case Node::Comparison::Greater: return ">"; case Node::Comparison::GreaterEqual: return ">="; } return "?"; } class Parser { public: Parser(std::string_view text, std::vector& paths) : text(text), paths(paths) {} std::shared_ptr Document() { auto node = Or(0); Skip(); if (!AtEnd()) Fail("unexpected text; join conditions with && or ||"); return node; } private: std::string_view text; std::vector& paths; std::size_t position = 0; [[noreturn]] void Fail(std::string_view what) const { throw ConditionError(position, what); } bool AtEnd() const { return position >= text.size(); } void Skip() { while (!AtEnd() && (text[position] == ' ' || text[position] == '\t')) ++position; } bool Match(std::string_view token) { Skip(); if (text.substr(position, token.size()) != token) return false; position += token.size(); return true; } std::shared_ptr Combine(Node::Op op, std::shared_ptr left, std::shared_ptr right) { auto node = std::make_shared(); node->op = op; node->left = std::move(left); node->right = std::move(right); return node; } std::shared_ptr Or(std::uint32_t depth) { auto left = And(depth); while (Match("||")) left = Combine(Node::Op::Or, std::move(left), And(depth)); return left; } std::shared_ptr And(std::uint32_t depth) { auto left = Not(depth); while (Match("&&")) left = Combine(Node::Op::And, std::move(left), Not(depth)); return left; } std::shared_ptr Not(std::uint32_t depth) { if (depth > maxDepth) Fail("nested too deeply"); Skip(); if (!AtEnd() && text[position] == '!' && text.substr(position, 2) != "!=") { ++position; return Combine(Node::Op::Not, Not(depth + 1), nullptr); } return Primary(depth); } std::shared_ptr Primary(std::uint32_t depth) { if (Match("(")) { auto inner = Or(depth + 1); if (!Match(")")) Fail("expected ')'"); return inner; } Skip(); const std::size_t start = position; auto node = std::make_shared(); node->a = Operand(); if (const auto comparison = ComparisonOperator()) { node->op = Node::Op::Compare; node->comparison = *comparison; node->b = Operand(); } else { node->op = Node::Op::Truthy; } node->source = std::string(text.substr(start, position - start)); while (!node->source.empty() && node->source.back() == ' ') node->source.pop_back(); return node; } std::optional ComparisonOperator() { Skip(); if (Match("==")) return Node::Comparison::Equal; if (Match("!=")) return Node::Comparison::NotEqual; if (Match("<=")) return Node::Comparison::LessEqual; if (Match(">=")) return Node::Comparison::GreaterEqual; if (Match("<")) return Node::Comparison::Less; if (Match(">")) return Node::Comparison::Greater; if (!AtEnd() && text[position] == '=') Fail("use == to compare"); return std::nullopt; } Node::Operand Operand() { Skip(); if (AtEnd()) Fail("expected a path or a value"); const char c = text[position]; if (c == '"' || c == '\'') return { {}, Value(String(c)) }; if ((c >= '0' && c <= '9') || c == '-') return { {}, Number() }; if (!PathStart(c)) Fail("expected a path or a value"); const std::size_t start = position; while (!AtEnd()) { const char next = text[position]; if (PathChar(next) || next == '.') { ++position; } else if (next == '[') { const std::size_t close = text.find(']', position); if (close == std::string_view::npos) Fail("expected ']'"); position = close + 1; } else { break; } } const std::string_view word = text.substr(start, position - start); if (word == "true") return { {}, Value(true) }; if (word == "false") return { {}, Value(false) }; if (word == "null") return { {}, Value() }; paths.emplace_back(word); return { std::string(word), Value() }; } std::string String(char quote) { ++position; std::string out; while (true) { if (AtEnd()) Fail("unterminated string"); const char c = text[position++]; if (c == quote) return out; if (c == '\\' && !AtEnd()) { out += text[position++]; continue; } out += c; } } Value Number() { const std::size_t start = position; if (text[position] == '-') ++position; while (!AtEnd() && ((text[position] >= '0' && text[position] <= '9') || text[position] == '.' || text[position] == 'e' || text[position] == 'E' || ((text[position] == '+' || text[position] == '-') && (text[position - 1] == 'e' || text[position - 1] == 'E')))) { ++position; } try { return Value::Parse(text.substr(start, position - start)); } catch (const ParseError&) { position = start; Fail("malformed number"); } } }; std::expected Load(const Node::Operand& operand, const Condition::Resolver& resolve) { if (operand.path.empty()) return operand.literal; return resolve(operand.path); } bool Truthy(const Value& value) { switch (value.Kind()) { case ValueKind::Null: return false; case ValueKind::Bool: return value.AsBool(); case ValueKind::Integer: return value.AsInteger() != 0; case ValueKind::Number: return value.AsNumber() != 0; case ValueKind::String: return !value.AsString().empty(); case ValueKind::Array: case ValueKind::Object: return value.Size() != 0; } return false; } template bool Ordered(Node::Comparison comparison, const T& a, const T& b) { switch (comparison) { case Node::Comparison::Less: return a < b; case Node::Comparison::LessEqual: return a <= b; case Node::Comparison::Greater: return a > b; case Node::Comparison::GreaterEqual: return a >= b; default: return false; } } Outcome Compare(const Node& node, const Value& a, const Value& b) { if (node.comparison == Node::Comparison::Equal) return a == b; if (node.comparison == Node::Comparison::NotEqual) return !(a == b); if (a.IsInteger() && b.IsInteger()) return Ordered(node.comparison, a.AsInteger(), b.AsInteger()); if (a.IsNumber() && b.IsNumber()) return Ordered(node.comparison, a.AsNumber(), b.AsNumber()); if (a.IsString() && b.IsString()) return Ordered(node.comparison, a.AsString(), b.AsString()); return std::unexpected(std::format("cannot order {} against {} in '{}': {} needs two numbers or two strings", a.Dump(), b.Dump(), node.source, ComparisonText(node.comparison))); } Outcome Evaluate(const Node& node, const Condition::Resolver& resolve) { switch (node.op) { case Node::Op::Or: { const Outcome left = Evaluate(*node.left, resolve); if (!left || *left) return left; return Evaluate(*node.right, resolve); } case Node::Op::And: { const Outcome left = Evaluate(*node.left, resolve); if (!left || !*left) return left; return Evaluate(*node.right, resolve); } case Node::Op::Not: { const Outcome inner = Evaluate(*node.left, resolve); if (!inner) return inner; return !*inner; } case Node::Op::Truthy: { const auto value = Load(node.a, resolve); if (!value) return std::unexpected(value.error()); return Truthy(*value); } case Node::Op::Compare: { const auto a = Load(node.a, resolve); if (!a) return std::unexpected(a.error()); const auto b = Load(node.b, resolve); if (!b) return std::unexpected(b.error()); return Compare(node, *a, *b); } } return std::unexpected("unknown node"); } } ConditionError::ConditionError(std::size_t offset, std::string_view what) : std::runtime_error(std::format("{} at offset {}", what, offset)), offset(offset) {} Condition Condition::Parse(std::string_view text) { Condition condition; condition.text = std::string(text); Parser parser(condition.text, condition.paths); condition.root = parser.Document(); return condition; } std::expected Condition::Evaluate(const Resolver& resolve) const { return ::Evaluate(*root, resolve); }