// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // The conditions `wait` and `pause --when` take: comparisons, && || ! and // parentheses with the usual precedence, truthiness of a lone operand, // strings in either quote, paths with indices, and short-circuiting. A // condition that cannot be decided says why instead of being false, since a // silently false condition makes `wait` hang: an unknown path, ordering a // string against a number. Malformed text is refused with its offset, and a // lone = is caught as the typo it is. import std; import Crafter.Debug; using namespace Crafter::Debug; namespace { std::int32_t failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } std::expected Resolve(std::string_view path) { static const Value state = Value::Parse(R"({"hp": 0, "scene": "Game", "paused": false, "bodies": [{"y": 2}, {"y": -12}], "name": "", "lives": 3})"); if (path.starts_with("bodies[")) { const std::size_t index = static_cast(path[7] - '0'); return state.Find("bodies")->Items()[index].Find("y") ? *state.Find("bodies")->Items()[index].Find("y") : Value(); } if (const Value* value = state.Find(path)) return *value; return std::unexpected(std::format("no watch '{}'", path)); } std::expected Evaluate(std::string_view text) { return Condition::Parse(text).Evaluate(Resolve); } bool Is(std::string_view text, bool expected) { const auto result = Evaluate(text); return result.has_value() && *result == expected; } std::size_t RefusedAt(std::string_view text) { try { (void)Condition::Parse(text); } catch (const ConditionError& error) { return error.offset; } return std::string_view::npos; } } int main() { Check(Is("hp <= 0", true), "a comparison with a literal"); Check(Is("scene == \"Game\" && !paused", true), "&& and ! combine"); Check(Is("scene == 'Menu' || lives >= 3", true), "|| and single-quoted strings"); Check(Is("hp > 0 || lives > 5", false), "false || false is false"); Check(Is("!(hp == 0 && lives == 3)", false), "parentheses group"); Check(Is("hp == 1 || hp == 0 && lives == 4", false), "&& binds tighter than ||"); Check(Is("bodies[1].y < -10", true), "a path with an index, against a negative number"); Check(Is("lives", true) && Is("name", false) && Is("paused", false), "a lone operand tests truthiness"); Check(Is("lives == 3.0", true), "an integer equals the same number"); Check(Is("hp > 0 && missing == 1", false), "&& short-circuits past an unknown path"); const auto unknown = Evaluate("missing > 1"); Check(!unknown && unknown.error() == "no watch 'missing'", "an unknown path is an error, not false"); const auto ordering = Evaluate("scene < 3"); Check(!ordering && ordering.error().find("cannot order") != std::string::npos && ordering.error().find("scene < 3") != std::string::npos, "ordering a string against a number is an error that quotes the comparison"); Check(Is("scene == 3", false), "== between different kinds is false, not an error"); Check(RefusedAt("hp = 0") == 3, "a lone = is refused where it stands"); Check(RefusedAt("(hp == 0") == 8, "an unclosed parenthesis is refused at the end"); Check(RefusedAt("hp == 0 lives") == 8, "two conditions without && or || are refused"); Check(RefusedAt("hp == ") == 6, "a missing operand is refused"); const Condition parsed = Condition::Parse("fort1.core.hp <= 0 && bodies[3].pos.y > 1"); Check(parsed.Paths().size() == 2 && parsed.Paths()[0] == "fort1.core.hp" && parsed.Paths()[1] == "bodies[3].pos.y", "a condition lists the paths it reads"); return failures == 0 ? 0 : 1; }