From d25e1a1e7f313efd78aa76c76e4fed5f4d792d8a Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Thu, 2 Aug 2018 13:48:47 -0400 Subject: Relax typing for comparison operators (#12537) * Relax typing for comparison operators Ports https://github.com/mapbox/mapbox-gl-js/pull/6961 * Review comments * Lint fixes --- cmake/core-files.cmake | 4 +- include/mbgl/style/expression/comparison.hpp | 68 ++++++ include/mbgl/style/expression/dsl.hpp | 1 + include/mbgl/style/expression/equals.hpp | 35 --- include/mbgl/style/expression/expression.hpp | 2 +- mapbox-gl-js | 2 +- src/mbgl/style/expression/comparison.cpp | 275 ++++++++++++++++++++++ src/mbgl/style/expression/compound_expression.cpp | 13 - src/mbgl/style/expression/dsl.cpp | 20 +- src/mbgl/style/expression/equals.cpp | 113 --------- src/mbgl/style/expression/parsing_context.cpp | 10 +- 11 files changed, 367 insertions(+), 176 deletions(-) create mode 100644 include/mbgl/style/expression/comparison.hpp delete mode 100644 include/mbgl/style/expression/equals.hpp create mode 100644 src/mbgl/style/expression/comparison.cpp delete mode 100644 src/mbgl/style/expression/equals.cpp diff --git a/cmake/core-files.cmake b/cmake/core-files.cmake index 44aff682fc..addacc174d 100644 --- a/cmake/core-files.cmake +++ b/cmake/core-files.cmake @@ -455,9 +455,9 @@ set(MBGL_CORE_FILES include/mbgl/style/expression/coercion.hpp include/mbgl/style/expression/collator.hpp include/mbgl/style/expression/collator_expression.hpp + include/mbgl/style/expression/comparison.hpp include/mbgl/style/expression/compound_expression.hpp include/mbgl/style/expression/dsl.hpp - include/mbgl/style/expression/equals.hpp include/mbgl/style/expression/error.hpp include/mbgl/style/expression/expression.hpp include/mbgl/style/expression/find_zoom_curve.hpp @@ -483,9 +483,9 @@ set(MBGL_CORE_FILES src/mbgl/style/expression/coalesce.cpp src/mbgl/style/expression/coercion.cpp src/mbgl/style/expression/collator_expression.cpp + src/mbgl/style/expression/comparison.cpp src/mbgl/style/expression/compound_expression.cpp src/mbgl/style/expression/dsl.cpp - src/mbgl/style/expression/equals.cpp src/mbgl/style/expression/expression.cpp src/mbgl/style/expression/find_zoom_curve.cpp src/mbgl/style/expression/get_covering_stops.cpp diff --git a/include/mbgl/style/expression/comparison.hpp b/include/mbgl/style/expression/comparison.hpp new file mode 100644 index 0000000000..cf64f5cd34 --- /dev/null +++ b/include/mbgl/style/expression/comparison.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace mbgl { +namespace style { +namespace expression { + +ParseResult parseComparison(const mbgl::style::conversion::Convertible&, ParsingContext&); + +class BasicComparison : public Expression { +public: + using CompareFunctionType = bool (*) (Value, Value); + + BasicComparison( + std::string op, + std::unique_ptr lhs, + std::unique_ptr rhs); + + void eachChild(const std::function& visit) const override; + bool operator==(const Expression&) const override; + EvaluationResult evaluate(const EvaluationContext&) const override; + std::vector> possibleOutputs() const override; + std::string getOperator() const override; + +private: + std::string op; + CompareFunctionType compare; + std::unique_ptr lhs; + std::unique_ptr rhs; + bool needsRuntimeTypeCheck; +}; + +class CollatorComparison : public Expression { +public: + using CompareFunctionType = bool (*) (std::string, std::string, Collator); + + CollatorComparison( + std::string op, + std::unique_ptr lhs, + std::unique_ptr rhs, + std::unique_ptr collator); + + void eachChild(const std::function& visit) const override; + bool operator==(const Expression&) const override; + EvaluationResult evaluate(const EvaluationContext&) const override; + std::vector> possibleOutputs() const override; + std::string getOperator() const override; + +private: + std::string op; + CompareFunctionType compare; + std::unique_ptr lhs; + std::unique_ptr rhs; + std::unique_ptr collator; + bool needsRuntimeTypeCheck; +}; + + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/dsl.hpp b/include/mbgl/style/expression/dsl.hpp index e9de20de18..a4483a6fe6 100644 --- a/include/mbgl/style/expression/dsl.hpp +++ b/include/mbgl/style/expression/dsl.hpp @@ -31,6 +31,7 @@ std::unique_ptr literal(Value value); std::unique_ptr literal(std::initializer_list value); std::unique_ptr literal(std::initializer_list value); +std::unique_ptr assertion(type::Type, std::unique_ptr); std::unique_ptr number(std::unique_ptr); std::unique_ptr string(std::unique_ptr); std::unique_ptr boolean(std::unique_ptr); diff --git a/include/mbgl/style/expression/equals.hpp b/include/mbgl/style/expression/equals.hpp deleted file mode 100644 index 1e8bf7acef..0000000000 --- a/include/mbgl/style/expression/equals.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -namespace mbgl { -namespace style { -namespace expression { - -class Equals : public Expression { -public: - Equals(std::unique_ptr lhs, std::unique_ptr rhs, optional> collator, bool negate); - - static ParseResult parse(const mbgl::style::conversion::Convertible&, ParsingContext&); - - void eachChild(const std::function& visit) const override; - bool operator==(const Expression&) const override; - EvaluationResult evaluate(const EvaluationContext&) const override; - std::vector> possibleOutputs() const override; - - std::string getOperator() const override { return negate ? "!=" : "=="; } -private: - std::unique_ptr lhs; - std::unique_ptr rhs; - optional> collator; - bool negate; -}; - -} // namespace expression -} // namespace style -} // namespace mbgl diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp index 8301f1572c..26124f6463 100644 --- a/include/mbgl/style/expression/expression.hpp +++ b/include/mbgl/style/expression/expression.hpp @@ -133,7 +133,7 @@ enum class Kind : int32_t { Case, Any, All, - Equals, + Comparison, }; class Expression { diff --git a/mapbox-gl-js b/mapbox-gl-js index dc43a09a6e..47e637c398 160000 --- a/mapbox-gl-js +++ b/mapbox-gl-js @@ -1 +1 @@ -Subproject commit dc43a09a6e84d7d97af33455594adf7f25dc961c +Subproject commit 47e637c3984a5121589bd17b51c6605f223aaeae diff --git a/src/mbgl/style/expression/comparison.cpp b/src/mbgl/style/expression/comparison.cpp new file mode 100644 index 0000000000..6179c3ce88 --- /dev/null +++ b/src/mbgl/style/expression/comparison.cpp @@ -0,0 +1,275 @@ +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +static bool isComparableType(const std::string& op, const type::Type& type) { + if (op == "==" || op == "!=") { + return type == type::String || + type == type::Number || + type == type::Boolean || + type == type::Null || + type == type::Value; + } else { + return type == type::String || + type == type::Number || + type == type::Value; + } +} + +bool eq(Value a, Value b) { return a == b; } +bool neq(Value a, Value b) { return a != b; } +bool lt(Value lhs, Value rhs) { + return lhs.match( + [&](const std::string& a) { return a < rhs.get(); }, + [&](double a) { return a < rhs.get(); }, + [&](const auto&) { assert(false); return false; } + ); +} +bool gt(Value lhs, Value rhs) { + return lhs.match( + [&](const std::string& a) { return a > rhs.get(); }, + [&](double a) { return a > rhs.get(); }, + [&](const auto&) { assert(false); return false; } + ); +} +bool lteq(Value lhs, Value rhs) { + return lhs.match( + [&](const std::string& a) { return a <= rhs.get(); }, + [&](double a) { return a <= rhs.get(); }, + [&](const auto&) { assert(false); return false; } + ); +} +bool gteq(Value lhs, Value rhs) { + return lhs.match( + [&](const std::string& a) { return a >= rhs.get(); }, + [&](double a) { return a >= rhs.get(); }, + [&](const auto&) { assert(false); return false; } + ); +} + +bool eqCollate(std::string a, std::string b, Collator c) { return c.compare(a, b) == 0; } +bool neqCollate(std::string a, std::string b, Collator c) { return !eqCollate(a, b, c); } +bool ltCollate(std::string a, std::string b, Collator c) { return c.compare(a, b) < 0; } +bool gtCollate(std::string a, std::string b, Collator c) { return c.compare(a, b) > 0; } +bool lteqCollate(std::string a, std::string b, Collator c) { return c.compare(a, b) <= 0; } +bool gteqCollate(std::string a, std::string b, Collator c) { return c.compare(a, b) >= 0; } + +static BasicComparison::CompareFunctionType getBasicCompareFunction(const std::string& op) { + if (op == "==") return eq; + else if (op == "!=") return neq; + else if (op == ">") return gt; + else if (op == "<") return lt; + else if (op == ">=") return gteq; + else if (op == "<=") return lteq; + assert(false); + return nullptr; +} + +static CollatorComparison::CompareFunctionType getCollatorComparisonFunction(const std::string& op) { + if (op == "==") return eqCollate; + else if (op == "!=") return neqCollate; + else if (op == ">") return gtCollate; + else if (op == "<") return ltCollate; + else if (op == ">=") return gteqCollate; + else if (op == "<=") return lteqCollate; + assert(false); + return nullptr; + +} + + +BasicComparison::BasicComparison( + std::string op_, + std::unique_ptr lhs_, + std::unique_ptr rhs_) + : Expression(Kind::Comparison, type::Boolean), + op(std::move(op_)), + compare(getBasicCompareFunction(op)), + lhs(std::move(lhs_)), + rhs(std::move(rhs_)) { + assert(isComparableType(op, lhs->getType()) && isComparableType(op, rhs->getType())); + assert(lhs->getType() == rhs->getType() || lhs->getType() == type::Value || rhs->getType() == type::Value); + + needsRuntimeTypeCheck = (op != "==" && op != "!=") && + (lhs->getType() == type::Value || rhs->getType() == type::Value); +} + +EvaluationResult BasicComparison::evaluate(const EvaluationContext& params) const { + EvaluationResult lhsResult = lhs->evaluate(params); + if (!lhsResult) return lhsResult; + + EvaluationResult rhsResult = rhs->evaluate(params); + if (!rhsResult) return rhsResult; + + if (needsRuntimeTypeCheck) { + type::Type lhsType = typeOf(*lhsResult); + type::Type rhsType = typeOf(*rhsResult); + // check that type is string or number, and equal + if (lhsType != rhsType || !(lhsType == type::String || lhsType == type::Number)) { + return EvaluationError { + R"(Expected arguments for ")" + op + R"(")" + + " to be (string, string) or (number, number), but found (" + toString(lhsType) + ", " + + toString(rhsType) + ") instead." + }; + } + } + + return compare(*lhsResult, *rhsResult); +} + +void BasicComparison::eachChild(const std::function& visit) const { + visit(*lhs); + visit(*rhs); +} + +std::string BasicComparison::getOperator() const { return op; } + +bool BasicComparison::operator==(const Expression& e) const { + if (e.getKind() == Kind::Comparison) { + auto comp = static_cast(&e); + return comp->op == op && + *comp->lhs == *lhs && + *comp->rhs == *rhs; + } + return false; +} + +std::vector> BasicComparison::possibleOutputs() const { + return {{ true }, { false }}; +} + +CollatorComparison::CollatorComparison( + std::string op_, + std::unique_ptr lhs_, + std::unique_ptr rhs_, + std::unique_ptr collator_) + : Expression(Kind::Comparison, type::Boolean), + op(op_), + compare(getCollatorComparisonFunction(op)), + lhs(std::move(lhs_)), + rhs(std::move(rhs_)), + collator(std::move(collator_)) { + assert(isComparableType(op, lhs->getType()) && isComparableType(op, rhs->getType())); + assert(lhs->getType() == rhs->getType() || lhs->getType() == type::Value || rhs->getType() == type::Value); + + needsRuntimeTypeCheck = (op == "==" || op == "!=") && + (lhs->getType() == type::Value || rhs->getType() == type::Value); +} + +EvaluationResult CollatorComparison::evaluate(const EvaluationContext& params) const { + EvaluationResult lhsResult = lhs->evaluate(params); + if (!lhsResult) return lhsResult; + + EvaluationResult rhsResult = rhs->evaluate(params); + if (!rhsResult) return lhsResult; + + if (needsRuntimeTypeCheck) { + if (typeOf(*lhsResult) != type::String || typeOf(*rhsResult) != type::String) { + return getBasicCompareFunction(op)(*lhsResult, *rhsResult); + } + } + + auto collatorResult = collator->evaluate(params); + if (!collatorResult) return collatorResult; + + const Collator& c = collatorResult->get(); + return compare(lhsResult->get(), rhsResult->get(), c); +} + +void CollatorComparison::eachChild(const std::function& visit) const { + visit(*lhs); + visit(*rhs); + visit(*collator); +} + +std::string CollatorComparison::getOperator() const { return op; } + +bool CollatorComparison::operator==(const Expression& e) const { + if (e.getKind() == Kind::Comparison) { + auto comp = static_cast(&e); + return comp->op == op && + *comp->collator == *collator && + *comp->lhs == *lhs && + *comp->rhs == *rhs; + } + return false; +} + +std::vector> CollatorComparison::possibleOutputs() const { + return {{ true }, { false }}; +} + +using namespace mbgl::style::conversion; +ParseResult parseComparison(const Convertible& value, ParsingContext& ctx) { + std::size_t length = arrayLength(value); + + if (length != 3 && length != 4) { + ctx.error("Expected two or three arguments."); + return ParseResult(); + } + + std::string op = *toString(arrayMember(value, 0)); + + assert(getBasicCompareFunction(op)); + + ParseResult lhs = ctx.parse(arrayMember(value, 1), 1, {type::Value}); + if (!lhs) return ParseResult(); + type::Type lhsType = (*lhs)->getType(); + if (!isComparableType(op, lhsType)) { + ctx.error(R"(")" + op + R"(" comparisons are not supported for type ')" + toString(lhsType) + "'.", 1); + return ParseResult(); + } + + ParseResult rhs = ctx.parse(arrayMember(value, 2), 2, {type::Value}); + if (!rhs) return ParseResult(); + type::Type rhsType = (*rhs)->getType(); + if (!isComparableType(op, rhsType)) { + ctx.error(R"(")" + op + R"(" comparisons are not supported for type ')" + toString(rhsType) + "'.", 2); + return ParseResult(); + } + + if ( + lhsType != rhsType && + lhsType != type::Value && + rhsType != type::Value + ) { + ctx.error("Cannot compare types '" + toString(lhsType) + "' and '" + toString(rhsType) + "'."); + return ParseResult(); + } + + if (op != "==" && op != "!=") { + // typing rules specific to less/greater than operators + if (lhsType == type::Value && rhsType != type::Value) { + // (value, T) + lhs = dsl::assertion(rhsType, std::move(*lhs)); + } else if (lhsType != type::Value && rhsType == type::Value) { + // (T, value) + rhs = dsl::assertion(lhsType, std::move(*rhs)); + } + } + + if (length == 4) { + if ( + lhsType != type::String && + rhsType != type::String && + lhsType != type::Value && + rhsType != type::Value + ) { + ctx.error("Cannot use collator to compare non-string types."); + return ParseResult(); + } + ParseResult collatorParseResult = ctx.parse(arrayMember(value, 3), 3, {type::Collator}); + if (!collatorParseResult) return ParseResult(); + return ParseResult(std::make_unique(op, std::move(*lhs), std::move(*rhs), std::move(*collatorParseResult))); + } + + return ParseResult(std::make_unique(op, std::move(*lhs), std::move(*rhs))); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/compound_expression.cpp b/src/mbgl/style/expression/compound_expression.cpp index bbb57e19ab..53ce91f0b7 100644 --- a/src/mbgl/style/expression/compound_expression.cpp +++ b/src/mbgl/style/expression/compound_expression.cpp @@ -485,19 +485,6 @@ std::unordered_map initiali define("ceil", [](double x) -> Result { return std::ceil(x); }); define("abs", [](double x) -> Result { return std::abs(x); }); - define(">", [](double lhs, double rhs) -> Result { return lhs > rhs; }); - define(">", [](const std::string& lhs, const std::string& rhs) -> Result { return lhs > rhs; }); - define(">", [](const std::string& lhs, const std::string& rhs, const Collator& c) -> Result { return c.compare(lhs, rhs) > 0; }); - define(">=", [](double lhs, double rhs) -> Result { return lhs >= rhs; }); - define(">=",[](const std::string& lhs, const std::string& rhs) -> Result { return lhs >= rhs; }); - define(">=", [](const std::string& lhs, const std::string& rhs, const Collator& c) -> Result { return c.compare(lhs, rhs) >= 0; }); - define("<", [](double lhs, double rhs) -> Result { return lhs < rhs; }); - define("<", [](const std::string& lhs, const std::string& rhs) -> Result { return lhs < rhs; }); - define("<", [](const std::string& lhs, const std::string& rhs, const Collator& c) -> Result { return c.compare(lhs, rhs) < 0; }); - define("<=", [](double lhs, double rhs) -> Result { return lhs <= rhs; }); - define("<=", [](const std::string& lhs, const std::string& rhs) -> Result { return lhs <= rhs; }); - define("<=", [](const std::string& lhs, const std::string& rhs, const Collator& c) -> Result { return c.compare(lhs, rhs) <= 0; }); - define("!", [](bool e) -> Result { return !e; }); define("is-supported-script", [](const std::string& x) -> Result { diff --git a/src/mbgl/style/expression/dsl.cpp b/src/mbgl/style/expression/dsl.cpp index a851d82e16..cdada583a2 100644 --- a/src/mbgl/style/expression/dsl.cpp +++ b/src/mbgl/style/expression/dsl.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include @@ -53,16 +53,20 @@ std::unique_ptr literal(std::initializer_list value) { return literal(values); } +std::unique_ptr assertion(type::Type type, std::unique_ptr value) { + return std::make_unique(type, vec(std::move(value))); +} + std::unique_ptr number(std::unique_ptr value) { - return std::make_unique(type::Number, vec(std::move(value))); + return assertion(type::Number, std::move(value)); } std::unique_ptr string(std::unique_ptr value) { - return std::make_unique(type::String, vec(std::move(value))); + return assertion(type::String, std::move(value)); } std::unique_ptr boolean(std::unique_ptr value) { - return std::make_unique(type::Boolean, vec(std::move(value))); + return assertion(type::Boolean, std::move(value)); } std::unique_ptr toColor(std::unique_ptr value) { @@ -91,22 +95,22 @@ std::unique_ptr zoom() { std::unique_ptr eq(std::unique_ptr lhs, std::unique_ptr rhs) { - return std::make_unique(std::move(lhs), std::move(rhs), nullopt, false); + return std::make_unique("==", std::move(lhs), std::move(rhs)); } std::unique_ptr ne(std::unique_ptr lhs, std::unique_ptr rhs) { - return std::make_unique(std::move(lhs), std::move(rhs), nullopt, true); + return std::make_unique("!=", std::move(lhs), std::move(rhs)); } std::unique_ptr gt(std::unique_ptr lhs, std::unique_ptr rhs) { - return compound(">", std::move(lhs), std::move(rhs)); + return std::make_unique(">", std::move(lhs), std::move(rhs)); } std::unique_ptr lt(std::unique_ptr lhs, std::unique_ptr rhs) { - return compound("<", std::move(lhs), std::move(rhs)); + return std::make_unique("<", std::move(lhs), std::move(rhs)); } std::unique_ptr step(std::unique_ptr input, diff --git a/src/mbgl/style/expression/equals.cpp b/src/mbgl/style/expression/equals.cpp deleted file mode 100644 index 73e2baf71b..0000000000 --- a/src/mbgl/style/expression/equals.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include -#include - -namespace mbgl { -namespace style { -namespace expression { - -static bool isComparableType(const type::Type& type) { - return type == type::String || - type == type::Number || - type == type::Boolean || - type == type::Null; -} - -Equals::Equals(std::unique_ptr lhs_, std::unique_ptr rhs_, optional> collator_, bool negate_) - : Expression(Kind::Equals, type::Boolean), - lhs(std::move(lhs_)), - rhs(std::move(rhs_)), - collator(std::move(collator_)), - negate(negate_) { - assert(isComparableType(lhs->getType()) || isComparableType(rhs->getType())); - assert(lhs->getType() == rhs->getType() || lhs->getType() == type::Value || rhs->getType() == type::Value); -} - -EvaluationResult Equals::evaluate(const EvaluationContext& params) const { - EvaluationResult lhsResult = lhs->evaluate(params); - if (!lhsResult) return lhsResult; - - EvaluationResult rhsResult = rhs->evaluate(params); - if (!rhsResult) return lhsResult; - - bool result; - - if (collator) { - auto collatorResult = (*collator)->evaluate(params); - const Collator& c = collatorResult->get(); - result = c.compare(lhsResult->get(), rhsResult->get()) == 0; - } else { - result = *lhsResult == *rhsResult; - } - if (negate) { - result = !result; - } - return result; -} - -void Equals::eachChild(const std::function& visit) const { - visit(*lhs); - visit(*rhs); - if (collator) { - visit(**collator); - } -} - -bool Equals::operator==(const Expression& e) const { - if (e.getKind() == Kind::Equals) { - auto eq = static_cast(&e); - return eq->negate == negate && *eq->lhs == *lhs && *eq->rhs == *rhs; - } - return false; -} - -std::vector> Equals::possibleOutputs() const { - return {{ true }, { false }}; -} - -using namespace mbgl::style::conversion; -ParseResult Equals::parse(const Convertible& value, ParsingContext& ctx) { - std::size_t length = arrayLength(value); - - if (length != 3 && length != 4) { - ctx.error("Expected two or three arguments."); - return ParseResult(); - } - - bool negate = toString(arrayMember(value, 0)) == std::string("!="); - - ParseResult lhs = ctx.parse(arrayMember(value, 1), 1, {type::Value}); - if (!lhs) return ParseResult(); - - ParseResult rhs = ctx.parse(arrayMember(value, 2), 2, {type::Value}); - if (!rhs) return ParseResult(); - - type::Type lhsType = (*lhs)->getType(); - type::Type rhsType = (*rhs)->getType(); - - if (!isComparableType(lhsType) && !isComparableType(rhsType)) { - ctx.error("Expected at least one argument to be a string, number, boolean, or null, but found (" + - toString(lhsType) + ", " + toString(rhsType) + ") instead."); - return ParseResult(); - } - - if (lhsType != rhsType && lhsType != type::Value && rhsType != type::Value) { - ctx.error("Cannot compare " + toString(lhsType) + " and " + toString(rhsType) + "."); - return ParseResult(); - } - - ParseResult collatorParseResult; - if (length == 4) { - if (lhsType != type::String && rhsType != type::String) { - ctx.error("Cannot use collator to compare non-string types."); - return ParseResult(); - } - collatorParseResult = ctx.parse(arrayMember(value, 3), 3, {type::Collator}); - if (!collatorParseResult) return ParseResult(); - } - - return ParseResult(std::make_unique(std::move(*lhs), std::move(*rhs), std::move(collatorParseResult), negate)); -} - -} // namespace expression -} // namespace style -} // namespace mbgl diff --git a/src/mbgl/style/expression/parsing_context.cpp b/src/mbgl/style/expression/parsing_context.cpp index 979eb58236..b3f6b1acee 100644 --- a/src/mbgl/style/expression/parsing_context.cpp +++ b/src/mbgl/style/expression/parsing_context.cpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include #include @@ -95,8 +95,12 @@ ParseResult ParsingContext::parse(const Convertible& value, std::size_t index_, const ExpressionRegistry& getExpressionRegistry() { static ExpressionRegistry registry {{ - {"==", Equals::parse}, - {"!=", Equals::parse}, + {"==", parseComparison}, + {"!=", parseComparison}, + {">", parseComparison}, + {"<", parseComparison}, + {">=", parseComparison}, + {"<=", parseComparison}, {"all", All::parse}, {"any", Any::parse}, {"array", ArrayAssertion::parse}, -- cgit v1.2.1