The app side of Crafter.Debug, minus the session:
- Command(name, description, handler): the handler takes nothing or
one argument struct and returns void, a value, Result<T> or
Async<T>. Arguments are decoded before it runs, so a mistyped call
is answered with every mistake and never reaches it; a throwing
handler becomes an Exception error instead of unwinding into the
frame loop. This replaces 3DForts' three-file pattern per verb.
- Watch(path, variable | getter [, setter]) with bounds, help and
read-only, and Sample(path, value) from any thread with a
frame-stamped history. Paths resolve into values ("bodies[1].pos.y")
and set the same way, writing a member writes the whole value back.
Unknown paths and commands get a suggestion.
- Group: registrations under a prefix that end with their owner. Not
movable, so neither is its owner: its watches refer to the owner's
members, and a move would leave them pointing at the old object.
- Log, LogWarn, LogError, Check and Emit on one frame-stamped
timeline, with source locations captured without macros. A check
failing every frame is counted every time but stored sparsely, and
the session's listener hears every failure.
- Condition: what `wait` and `pause --when` evaluate each frame. A
condition that cannot be decided reports why instead of being false,
which would make a wait hang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
314 lines
11 KiB
C++
314 lines
11 KiB
C++
// 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<const Node> left;
|
|
std::shared_ptr<const Node> 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<bool, std::string>;
|
|
|
|
// 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<std::string>& paths) : text(text), paths(paths) {}
|
|
|
|
std::shared_ptr<const Node> Document() {
|
|
auto node = Or(0);
|
|
Skip();
|
|
if (!AtEnd()) Fail("unexpected text; join conditions with && or ||");
|
|
return node;
|
|
}
|
|
|
|
private:
|
|
std::string_view text;
|
|
std::vector<std::string>& 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<const Node> Combine(Node::Op op, std::shared_ptr<const Node> left, std::shared_ptr<const Node> right) {
|
|
auto node = std::make_shared<Node>();
|
|
node->op = op;
|
|
node->left = std::move(left);
|
|
node->right = std::move(right);
|
|
return node;
|
|
}
|
|
|
|
std::shared_ptr<const Node> 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<const Node> 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<const Node> 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<const Node> 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>();
|
|
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<Node::Comparison> 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<Value, std::string> 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<class T>
|
|
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<bool, std::string> Condition::Evaluate(const Resolver& resolve) const {
|
|
return ::Evaluate(*root, resolve);
|
|
}
|