Files
taskwarrior-2.x/src/Eval.cpp
2024-07-29 22:34:51 +02:00

735 lines
25 KiB
C++
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2013 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
// cmake.h include header must come first
#include <Color.h>
#include <Context.h>
#include <DOM.h>
#include <Eval.h>
#include <Task.h>
#include <format.h>
#include <shared.h>
#include <time.h>
#include <map>
////////////////////////////////////////////////////////////////////////////////
// Supported operators, borrowed from C++, particularly the precedence.
// Note: table is sorted by length of operator string, so searches match
// longest first.
static struct {
std::string op;
int precedence;
char type; // b=binary, u=unary, c=circumfix
char associativity; // l=left, r=right, _=?
} operators[] = {
// Operator Precedence Type Associativity
{"^", 16, 'b', 'r'}, // Exponent
{"!", 15, 'u', 'r'}, // Unary not
{"_neg_", 15, 'u', 'r'}, // Unary minus
{"_pos_", 15, 'u', 'r'}, // Unary plus
{"_hastag_", 14, 'b', 'l'}, // +tag [Pseudo-op]
{"_notag_", 14, 'b', 'l'}, // -tag [Pseudo-op]
{"*", 13, 'b', 'l'}, // Multiplication
{"/", 13, 'b', 'l'}, // Division
{"%", 13, 'b', 'l'}, // Modulus
{"+", 12, 'b', 'l'}, // Addition
{"-", 12, 'b', 'l'}, // Subtraction
{"<=", 10, 'b', 'l'}, // Less than or equal
{">=", 10, 'b', 'l'}, // Greater than or equal
{">", 10, 'b', 'l'}, // Greater than
{"<", 10, 'b', 'l'}, // Less than
{"=", 9, 'b', 'l'}, // Equal (partial)
{"==", 9, 'b', 'l'}, // Equal (exact)
{"!=", 9, 'b', 'l'}, // Inequal (partial)
{"!==", 9, 'b', 'l'}, // Inequal (exact)
{"~", 8, 'b', 'l'}, // Regex match
{"!~", 8, 'b', 'l'}, // Regex non-match
{"and", 5, 'b', 'l'}, // Conjunction
{"or", 4, 'b', 'l'}, // Disjunction
{"xor", 3, 'b', 'l'}, // Disjunction
{"(", 0, 'c', '_'}, // Precedence start
{")", 0, 'c', '_'}, // Precedence end
};
#define NUM_OPERATORS (sizeof(operators) / sizeof(operators[0]))
////////////////////////////////////////////////////////////////////////////////
// Built-in support for some named constants.
static bool namedConstants(const std::string& name, Variant& value) {
if (name == "true")
value = Variant(true);
else if (name == "false")
value = Variant(false);
else if (name == "pi")
value = Variant(3.14159165);
else
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Support for evaluating DOM references (add with `e.AddSource(domSource)`)
bool domSource(const std::string& identifier, Variant& value) {
if (getDOM(identifier, Context::getContext().currentTask, value)) {
value.source(identifier);
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
Eval::Eval() { addSource(namedConstants); }
////////////////////////////////////////////////////////////////////////////////
void Eval::addSource(bool (*source)(const std::string&, Variant&)) { _sources.push_back(source); }
////////////////////////////////////////////////////////////////////////////////
void Eval::evaluateInfixExpression(const std::string& e, Variant& v) const {
// Reduce e to a vector of tokens.
Lexer l(e);
std::vector<std::pair<std::string, Lexer::Type>> tokens;
std::string token;
Lexer::Type type;
while (l.token(token, type)) tokens.emplace_back(token, type);
// Parse for syntax checking and operator replacement.
if (_debug) Context::getContext().debug("FILTER Infix " + dump(tokens));
infixParse(tokens);
if (_debug) Context::getContext().debug("FILTER Infix parsed " + dump(tokens));
// Convert infix --> postfix.
infixToPostfix(tokens);
if (_debug) Context::getContext().debug("FILTER Postfix " + dump(tokens));
// Call the postfix evaluator.
evaluatePostfixStack(tokens, v);
}
////////////////////////////////////////////////////////////////////////////////
void Eval::evaluatePostfixExpression(const std::string& e, Variant& v) const {
// Reduce e to a vector of tokens.
Lexer l(e);
std::vector<std::pair<std::string, Lexer::Type>> tokens;
std::string token;
Lexer::Type type;
while (l.token(token, type)) tokens.emplace_back(token, type);
if (_debug) Context::getContext().debug("FILTER Postfix " + dump(tokens));
// Call the postfix evaluator.
evaluatePostfixStack(tokens, v);
}
////////////////////////////////////////////////////////////////////////////////
void Eval::compileExpression(const std::vector<std::pair<std::string, Lexer::Type>>& precompiled) {
_compiled = precompiled;
// Parse for syntax checking and operator replacement.
if (_debug) Context::getContext().debug("FILTER Infix " + dump(_compiled));
infixParse(_compiled);
if (_debug) Context::getContext().debug("FILTER Infix parsed " + dump(_compiled));
// Convert infix --> postfix.
infixToPostfix(_compiled);
if (_debug) Context::getContext().debug("FILTER Postfix " + dump(_compiled));
}
////////////////////////////////////////////////////////////////////////////////
void Eval::evaluateCompiledExpression(Variant& v) {
// Call the postfix evaluator.
evaluatePostfixStack(_compiled, v);
}
////////////////////////////////////////////////////////////////////////////////
void Eval::debug(bool value) { _debug = value; }
////////////////////////////////////////////////////////////////////////////////
// Static.
std::vector<std::string> Eval::getOperators() {
std::vector<std::string> all;
all.reserve(NUM_OPERATORS);
for (const auto& opr : operators) all.push_back(opr.op);
return all;
}
////////////////////////////////////////////////////////////////////////////////
// Static.
std::vector<std::string> Eval::getBinaryOperators() {
std::vector<std::string> all;
for (const auto& opr : operators)
if (opr.type == 'b') all.push_back(opr.op);
return all;
}
////////////////////////////////////////////////////////////////////////////////
void Eval::evaluatePostfixStack(const std::vector<std::pair<std::string, Lexer::Type>>& tokens,
Variant& result) const {
if (tokens.size() == 0) throw std::string("No expression to evaluate.");
// This is stack used by the postfix evaluator.
std::vector<Variant> values;
values.reserve(tokens.size());
for (const auto& token : tokens) {
// Unary operators.
if (token.second == Lexer::Type::op && token.first == "!") {
if (values.size() < 1) throw std::string("The expression could not be evaluated.");
Variant right = values.back();
values.pop_back();
Variant result = !right;
values.push_back(result);
if (_debug)
Context::getContext().debug(format("Eval {1} ↓'{2}' → ↑'{3}'", token.first,
(std::string)right, (std::string)result));
} else if (token.second == Lexer::Type::op && token.first == "_neg_") {
if (values.size() < 1) throw std::string("The expression could not be evaluated.");
Variant right = values.back();
values.pop_back();
Variant result(0);
result -= right;
values.push_back(result);
if (_debug)
Context::getContext().debug(format("Eval {1} ↓'{2}' → ↑'{3}'", token.first,
(std::string)right, (std::string)result));
} else if (token.second == Lexer::Type::op && token.first == "_pos_") {
// The _pos_ operator is a NOP.
if (_debug)
Context::getContext().debug(format("[{1}] eval op {2} NOP", values.size(), token.first));
}
// Binary operators.
else if (token.second == Lexer::Type::op) {
if (values.size() < 2) throw std::string("The expression could not be evaluated.");
Variant right = values.back();
values.pop_back();
Variant left = values.back();
values.pop_back();
auto contextTask = Context::getContext().currentTask;
// Ordering these by anticipation frequency of use is a good idea.
Variant result;
if (token.first == "and")
result = left && right;
else if (token.first == "or")
result = left || right;
else if (token.first == "&&")
result = left && right;
else if (token.first == "||")
result = left || right;
else if (token.first == "<")
result = left < right;
else if (token.first == "<=")
result = left <= right;
else if (token.first == ">")
result = left > right;
else if (token.first == ">=")
result = left >= right;
else if (token.first == "==")
result = left.operator==(right);
else if (token.first == "!==")
result = left.operator!=(right);
else if (token.first == "=")
result = left.operator_partial(right);
else if (token.first == "!=")
result = left.operator_nopartial(right);
else if (token.first == "+")
result = left + right;
else if (token.first == "-")
result = left - right;
else if (token.first == "*")
result = left * right;
else if (token.first == "/")
result = left / right;
else if (token.first == "^")
result = left ^ right;
else if (token.first == "%")
result = left % right;
else if (token.first == "xor")
result = left.operator_xor(right);
else if (contextTask) {
if (token.first == "~")
result = left.operator_match(right, *contextTask);
else if (token.first == "!~")
result = left.operator_nomatch(right, *contextTask);
else if (token.first == "_hastag_")
result = left.operator_hastag(right, *contextTask);
else if (token.first == "_notag_")
result = left.operator_notag(right, *contextTask);
else
throw format("Unsupported operator '{1}'.", token.first);
} else
throw format("Unsupported operator '{1}'.", token.first);
values.push_back(result);
if (_debug)
Context::getContext().debug(format("Eval ↓'{1}' {2} ↓'{3}' → ↑'{4}'", (std::string)left,
token.first, (std::string)right, (std::string)result));
}
// Literals and identifiers.
else {
Variant v(token.first);
switch (token.second) {
case Lexer::Type::number:
if (Lexer::isAllDigits(token.first)) {
v.cast(Variant::type_integer);
if (_debug)
Context::getContext().debug(format("Eval literal number ↑'{1}'", (std::string)v));
} else {
v.cast(Variant::type_real);
if (_debug)
Context::getContext().debug(format("Eval literal decimal ↑'{1}'", (std::string)v));
}
break;
case Lexer::Type::op:
throw std::string("Operator expected.");
break;
case Lexer::Type::dom:
case Lexer::Type::identifier: {
bool found = false;
for (const auto& source : _sources) {
if (source(token.first, v)) {
if (_debug)
Context::getContext().debug(
format("Eval identifier source '{1}' → ↑'{2}'", token.first, (std::string)v));
found = true;
break;
}
}
// An identifier that fails lookup is a string.
if (!found) {
v.cast(Variant::type_string);
if (_debug)
Context::getContext().debug(
format("Eval identifier source failed '{1}'", token.first));
}
} break;
case Lexer::Type::date:
v.cast(Variant::type_date);
if (_debug)
Context::getContext().debug(format("Eval literal date ↑'{1}'", (std::string)v));
break;
case Lexer::Type::duration:
v.cast(Variant::type_duration);
if (_debug)
Context::getContext().debug(format("Eval literal duration ↑'{1}'", (std::string)v));
break;
// Nothing to do.
case Lexer::Type::string:
default:
if (_debug)
Context::getContext().debug(format("Eval literal string ↑'{1}'", (std::string)v));
break;
}
values.push_back(v);
}
}
// If there is more than one variant left on the stack, then the original
// expression was not valid.
if (values.size() != 1) throw std::string("The value is not an expression.");
result = values[0];
}
////////////////////////////////////////////////////////////////////////////////
//
// Grammar:
// Logical --> Regex {( "and" | "or" | "xor" ) Regex}
// Regex --> Equality {( "~" | "!~" ) Equality}
// Equality --> Comparative {( "==" | "=" | "!=" ) Comparative}
// Comparative --> Arithmetic {( "<=" | "<" | ">=" | ">" ) Arithmetic}
// Arithmetic --> Geometric {( "+" | "-" ) Geometric}
// Geometric --> Tag {( "*" | "/" | "%" ) Tag}
// Tag --> Unary {( "_hastag_" | "_notag_" ) Unary}
// Unary --> [( "-" | "+" | "!" )] Exponent
// Exponent --> Primitive ["^" Primitive]
// Primitive --> "(" Logical ")" | Variant
//
void Eval::infixParse(std::vector<std::pair<std::string, Lexer::Type>>& infix) const {
unsigned int i = 0;
parseLogical(infix, i);
}
////////////////////////////////////////////////////////////////////////////////
// Logical --> Regex {( "and" | "or" | "xor" ) Regex}
bool Eval::parseLogical(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parseRegex(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op &&
(infix[i].first == "and" || infix[i].first == "or" || infix[i].first == "xor")) {
++i;
if (!parseRegex(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Regex --> Equality {( "~" | "!~" ) Equality}
bool Eval::parseRegex(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parseEquality(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op &&
(infix[i].first == "~" || infix[i].first == "!~")) {
++i;
if (!parseEquality(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Equality --> Comparative {( "==" | "=" | "!==" | "!=" ) Comparative}
bool Eval::parseEquality(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parseComparative(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op &&
(infix[i].first == "==" || infix[i].first == "=" || infix[i].first == "!==" ||
infix[i].first == "!=")) {
++i;
if (!parseComparative(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Comparative --> Arithmetic {( "<=" | "<" | ">=" | ">" ) Arithmetic}
bool Eval::parseComparative(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parseArithmetic(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op &&
(infix[i].first == "<=" || infix[i].first == "<" || infix[i].first == ">=" ||
infix[i].first == ">")) {
++i;
if (!parseArithmetic(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Arithmetic --> Geometric {( "+" | "-" ) Geometric}
bool Eval::parseArithmetic(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parseGeometric(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op &&
(infix[i].first == "+" || infix[i].first == "-")) {
++i;
if (!parseGeometric(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Geometric --> Tag {( "*" | "/" | "%" ) Tag}
bool Eval::parseGeometric(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parseTag(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op &&
(infix[i].first == "*" || infix[i].first == "/" || infix[i].first == "%")) {
++i;
if (!parseTag(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Tag --> Unary {( "_hastag_" | "_notag_" ) Unary}
bool Eval::parseTag(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parseUnary(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op &&
(infix[i].first == "_hastag_" || infix[i].first == "_notag_")) {
++i;
if (!parseUnary(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Unary --> [( "-" | "+" | "!" )] Exponent
bool Eval::parseUnary(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size()) {
if (infix[i].first == "-") {
infix[i].first = "_neg_";
++i;
} else if (infix[i].first == "+") {
infix[i].first = "_pos_";
++i;
} else if (infix[i].first == "!") {
++i;
}
}
return parseExponent(infix, i);
}
////////////////////////////////////////////////////////////////////////////////
// Exponent --> Primitive ["^" Primitive]
bool Eval::parseExponent(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size() && parsePrimitive(infix, i)) {
while (i < infix.size() && infix[i].second == Lexer::Type::op && infix[i].first == "^") {
++i;
if (!parsePrimitive(infix, i)) return false;
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Primitive --> "(" Logical ")" | Variant
bool Eval::parsePrimitive(std::vector<std::pair<std::string, Lexer::Type>>& infix,
unsigned int& i) const {
if (i < infix.size()) {
if (infix[i].first == "(") {
++i;
if (i < infix.size() && parseLogical(infix, i)) {
if (i < infix.size() && infix[i].first == ")") {
++i;
return true;
}
}
} else {
bool found = false;
for (const auto& source : _sources) {
Variant v;
if (source(infix[i].first, v)) {
found = true;
break;
}
}
if (found) {
++i;
return true;
} else if (infix[i].second != Lexer::Type::op) {
++i;
return true;
}
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Dijkstra Shunting Algorithm.
// https://en.wikipedia.org/wiki/Shunting-yard_algorithm
//
// While there are tokens to be read:
// Read a token.
// If the token is an operator, o1, then:
// while there is an operator token, o2, at the top of the stack, and
// either o1 is left-associative and its precedence is less than or
// equal to that of o2,
// or o1 is right-associative and its precedence is less than that
// of o2,
// pop o2 off the stack, onto the output queue;
// push o1 onto the stack.
// If the token is a left parenthesis, then push it onto the stack.
// If the token is a right parenthesis:
// Until the token at the top of the stack is a left parenthesis, pop
// operators off the stack onto the output queue.
// Pop the left parenthesis from the stack, but not onto the output queue.
// If the token at the top of the stack is a function token, pop it onto
// the output queue.
// If the stack runs out without finding a left parenthesis, then there
// are mismatched parentheses.
// If the token is a number, then add it to the output queue.
//
// When there are no more tokens to read:
// While there are still operator tokens in the stack:
// If the operator token on the top of the stack is a parenthesis, then
// there are mismatched parentheses.
// Pop the operator onto the output queue.
// Exit.
//
void Eval::infixToPostfix(std::vector<std::pair<std::string, Lexer::Type>>& infix) const {
// Short circuit.
if (infix.size() == 1) return;
// Result.
std::vector<std::pair<std::string, Lexer::Type>> postfix;
// Shunting yard.
std::vector<std::pair<std::string, Lexer::Type>> op_stack;
// Operator characteristics.
char type;
unsigned int precedence;
char associativity;
for (auto& token : infix) {
if (token.second == Lexer::Type::op && token.first == "(") {
op_stack.push_back(token);
} else if (token.second == Lexer::Type::op && token.first == ")") {
while (op_stack.size() && op_stack.back().first != "(") {
postfix.push_back(op_stack.back());
op_stack.pop_back();
}
if (op_stack.size())
op_stack.pop_back();
else
throw std::string("Mismatched parentheses in expression");
} else if (token.second == Lexer::Type::op &&
identifyOperator(token.first, type, precedence, associativity)) {
char type2;
unsigned int precedence2;
char associativity2;
while (op_stack.size() > 0 &&
identifyOperator(op_stack.back().first, type2, precedence2, associativity2) &&
((associativity == 'l' && precedence <= precedence2) ||
(associativity == 'r' && precedence < precedence2))) {
postfix.push_back(op_stack.back());
op_stack.pop_back();
}
op_stack.push_back(token);
} else {
postfix.push_back(token);
}
}
while (op_stack.size()) {
if (op_stack.back().first == "(" || op_stack.back().first == ")")
throw std::string("Mismatched parentheses in expression");
postfix.push_back(op_stack.back());
op_stack.pop_back();
}
infix = postfix;
}
////////////////////////////////////////////////////////////////////////////////
bool Eval::identifyOperator(const std::string& op, char& type, unsigned int& precedence,
char& associativity) const {
for (const auto& opr : operators) {
if (opr.op == op) {
type = opr.type;
precedence = opr.precedence;
associativity = opr.associativity;
return true;
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
std::string Eval::dump(std::vector<std::pair<std::string, Lexer::Type>>& tokens) const {
// Set up a color mapping.
std::map<Lexer::Type, Color> color_map;
color_map[Lexer::Type::op] = Color("gray14 on gray6");
color_map[Lexer::Type::number] = Color("rgb530 on gray6");
color_map[Lexer::Type::hex] = Color("rgb303 on gray6");
color_map[Lexer::Type::string] = Color("rgb550 on gray6");
color_map[Lexer::Type::dom] = Color("rgb045 on gray6");
color_map[Lexer::Type::identifier] = Color("rgb035 on gray6");
color_map[Lexer::Type::date] = Color("rgb150 on gray6");
color_map[Lexer::Type::duration] = Color("rgb531 on gray6");
std::string output;
for (auto i = tokens.begin(); i != tokens.end(); ++i) {
if (i != tokens.begin()) output += ' ';
Color c;
if (color_map[i->second].nontrivial())
c = color_map[i->second];
else
c = Color("rgb000 on gray6");
output += c.colorize(i->first);
}
return output;
}
////////////////////////////////////////////////////////////////////////////////