c++boost-spirit

How to parse the logical expression in boost::spirit


The parser parses the expression which involves binary operators (+,-,*,/) in the right precedence. However I can't get it to work with the logical expressions (&&,||).

For example, the result for the following expression should be 1 (true) (currently its 0):

1 || 0 && 0

The rule I wrote currently parses from left to right with no && precedence:

expression = (uint_[_val = _1] | '(' >> expression[_val = _1] >> ')') * (("&&" >> (uint_[_val = (_val && _1)] | expression[_val = (_val && _1)])) | ("||" >> (uint_[_val = (_val || _1)] | expression[_val = (_val || _1)])));

Moreover, the rule currently does not do short-circuiting.


Solution

  • If you want heterogeneous evaluations (not just boolean predicates), unary operators, precedence, associativity, look at some examples I made on this site.

    Usually they separate the evaluation stage from the parsing stage:

    Short-cutting

    As long as you conflate parsing with evaluation, there can NEVER be any useful sense of "short-cutting" since the parsing can not be skipped regardless.

    Seeing the example from Boolean expression (grammar) parser in c++ and Support implied AND operation in boost spirit boolean expression parser, you can use nested rules to impose precedence:

        or_ = (xor_ >> qr::distinct(alnum | '_')["||"] >> or_) //
                  [_val = phx::construct<binop<op_or>>(_1, _2)] |
            xor_[_val = _1];
        xor_ = (and_ >> qr::distinct(alnum | '_')["^"] >> xor_) //
                   [_val = phx::construct<binop<op_xor>>(_1, _2)] |
            and_[_val = _1];
        and_ = (not_ >> -qr::distinct(alnum | '_')["&&"] >> and_) //
                   [_val = phx::construct<binop<op_and>>(_1, _2)] |
            not_[_val = _1];
        not_ = (qr::distinct(alnum | '_')["!"] > simple) //
                   [_val = phx::construct<unop<op_not>>(_1)] |
            simple[_val = _1];
    

    Now this creates an AST that can be re-evaluated later. Because parsing ISN'T conflated with evaluation, shortcut simplification can be done.

    Here's a demo adapted towards your question:

    Live On Coliru

    // #define BOOST_SPIRIT_DEBUG
    #include <boost/phoenix.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <boost/spirit/repository/include/qi_distinct.hpp>
    namespace qi  = boost::spirit::qi;
    namespace qr  = boost::spirit::repository::qi;
    namespace phx = boost::phoenix;
    
    using var = std::string;
    template <typename> struct binop;
    template <typename> struct unop;
    
    typedef boost::variant<var,                                            //
                           bool,                                           //
                           boost::recursive_wrapper<unop<struct op_not>>,  //
                           boost::recursive_wrapper<binop<struct op_and>>, //
                           boost::recursive_wrapper<binop<struct op_xor>>, //
                           boost::recursive_wrapper<binop<struct op_or>>   //
                           >
        expr;
    
    template <typename> struct binop {
        expr oper1, oper2;
        auto operator<=>(binop const&) const = default;
    };
    
    template <typename> struct unop {
        expr oper1;
        auto operator<=>(unop const&) const = default;
    };
    
    struct print_f {
        std::ostream& _os;
    
        void operator()(expr const& e) const { boost::apply_visitor(*this, e); }
        void operator()(bool const& b) const { _os << (b?1:0); }
        void operator()(var const& v) const { _os << v; }
        void operator()(binop<op_and> const& b) const { print(" && ", b.oper1, b.oper2); }
        void operator()(binop<op_or> const& b) const { print(" || ", b.oper1, b.oper2); }
        void operator()(binop<op_xor> const& b) const { print(" ^ ", b.oper1, b.oper2); }
    
        void print(std::string const& op, expr const& l, expr const& r) const {
            _os << "("; operator()(l); _os << op; operator()(r); _os << ")";
        }
    
        void operator()(unop<op_not> const& u) const {
            _os << "("; _os << "!"; operator()(u.oper1); _os << ")";
        }
    };
    
    struct simplify_f {
        expr operator()(expr const& e) const {
            auto simplified = boost::apply_visitor(*this, e);
            if (simplified != e)
                std::cout << " - simplified: " << e << " -> " << simplified << "\n";
            return simplified;
        }
    
        expr operator()(auto const& v) const { return v; }
    
        expr operator()(binop<op_and> const& b) const {
            auto lhs = operator()(b.oper1);
            auto rhs = operator()(b.oper2);
            if (static_false(lhs)) return rhs;
            if (static_false(rhs)) return lhs;
            if (lhs == rhs) return lhs; // a && a == a
            return binop<op_and>{lhs, rhs};
        }
        expr operator()(binop<op_or> const& b) const {
            auto lhs = operator()(b.oper1);
            auto rhs = operator()(b.oper2);
            if (static_true(lhs) || static_true(rhs)) return true;
            if (static_false(lhs)) return rhs;
            if (static_false(rhs)) return lhs;
            if (lhs == rhs) return lhs; // a || a == a
            return binop<op_or>{lhs, rhs};
        }
        expr operator()(binop<op_xor> const& b) const {
            auto lhs = operator()(b.oper1);
            auto rhs = operator()(b.oper2);
            if (static_true(lhs))  return operator()(unop<op_not>(rhs));
            if (static_true(rhs))  return operator()(unop<op_not>(lhs));
            if (static_false(lhs)) return rhs;
            if (static_false(rhs)) return lhs;
            if (lhs == rhs) return false; // a ^ a == false
            if (lhs == expr{unop<op_not>(rhs)}) return true; // a ^ !a == true
            if (rhs == expr{unop<op_not>(lhs)}) return true; // !a ^ a == true
    
            return binop<op_xor>{lhs, rhs};
        }
        expr operator()(unop<op_not> const& u) const {
            auto lhs = operator()(u.oper1);
            if (static_true(lhs))  return false;
            if (static_false(lhs)) return true;
            return unop<op_not>{lhs};
        }
    
      private:
        static bool static_true(expr const& e) { return e == expr{true}; }
        static bool static_false(expr const& e) { return e == expr{false}; }
    };
    
    enum Trace { verbose, silent };
    template <typename Map> struct eval_f {
        Map&  map_;
        Trace trace_ = verbose;
    
        bool operator()(expr          const& e) const {
            auto val = boost::apply_visitor(*this, e);
            if (trace_ == verbose && (e != expr{val}))
                std::cout << " - trace eval: " << e << " -> " << val << "\n";
            return val;
        } 
        bool operator()(bool          const& b) const { return b;                                          } 
        bool operator()(var           const& v) const { return map_[v];                                    }
        bool operator()(binop<op_and> const& b) const { return operator()(b.oper1) && operator()(b.oper2); }
        bool operator()(binop<op_or>  const& b) const { return operator()(b.oper1) || operator()(b.oper2); }
        bool operator()(binop<op_xor> const& b) const { return operator()(b.oper1) != operator()(b.oper2); } 
        bool operator()(unop<op_not>  const& u) const { return !operator()(u.oper1);                       }
    };
    
    std::ostream& operator<<(std::ostream& os, expr const& e) {
        boost::apply_visitor(print_f(os), e);
        return os;
    }
    
    template <typename It, typename Skipper = qi::space_type> struct parser : qi::grammar<It, expr(), Skipper> {
        parser() : parser::base_type(expr_) {
            using namespace qi;
    
            expr_ = or_.alias();
    
            or_ = (xor_ >> qr::distinct(alnum | '_')["||"] >> or_) //
                      [_val = phx::construct<binop<op_or>>(_1, _2)] | xor_[_val = _1];
            xor_ = (and_ >> qr::distinct(alnum | '_')["^"] >> xor_) //
                       [_val = phx::construct<binop<op_xor>>(_1, _2)] | and_[_val = _1];
            and_ = (not_ >> qr::distinct(alnum | '_')["&&"] >> and_) //
                      [_val = phx::construct<binop<op_and>>(_1, _2)] | not_[_val = _1];
            not_ = (qr::distinct(alnum | '_')["!"] > simple) //
                       [_val = phx::construct<unop<op_not>>(_1)] | simple[_val = _1];
    
            simple = (('(' > expr_ > ')') | lit_ | var_);
            lit_   = qi::uint_parser<bool, 2, 1, 1>{} | qi::bool_;
            var_   = qr::distinct(alnum | '_')[alpha >> *(alnum | char_("_"))];
    
            BOOST_SPIRIT_DEBUG_NODES((expr_)(or_)(xor_)(and_)(not_)(simple)(var_)(lit_));
        }
    
      private:
        qi::rule<It, var()>           var_;
        qi::rule<It, bool()>          lit_;
        qi::rule<It, expr(), Skipper> not_, and_, xor_, or_, simple, expr_;
    };
    
    int main() {
        std::cout << std::boolalpha;
        static parser<std::string::const_iterator> const p;
        static simplify_f const simplify;
    
        for (auto const& input : std::list<std::string>{
                 "1 || 0 && 0",
    
                 "(a && b) ^ ((c && d) || (a && b))",
    
                 "a && b ^ c && d || a && b",
    
                 /// Simpler tests:
                 "a && b",
                 "! a && b",
    
                 "! (a && b)",
    
                 "a || b || c",
                 "(a && b)",
                 "a ^ b",
                 "a || b",
                 "(a) || (b)",
                 "((c && d) || (a && b))",
             }) {
            auto f(std::begin(input)), l(std::end(input));
    
            try {
                expr parsed;
                bool ok = qi::phrase_parse(f, l, p, qi::space, parsed);
    
                std::cout << "\n======= input '" << input << "'\n";
    
                if (!ok)
                    std::cerr << "invalid input\n";
                else {
                    std::cout << "result:     " << parsed << "\n";
                    auto simplified = simplify(parsed);
                    if (simplified != parsed)
                        std::cout << "simplified: " << simplified << "\n";
    
                    std::map<std::string, bool> vars {{"a", true}, {"b", true}, {"c", true}, {"d", false}};
                    auto value = eval_f{vars}(simplified);
                    std::cout << "evaluated:  " << value << "\n";
    
                    auto original = eval_f{vars, silent}(parsed);
                    assert(original == value); // simplify should not change the result
                }
    
            } catch (qi::expectation_failure<decltype(f)> const& e) {
                std::cerr << "expectation_failure at '" << std::string(e.first, e.last) << "'\n";
            }
    
            if (f != l)
                std::cerr << "unparsed: '" << std::string(f, l) << "'\n";
        }
    }
    

    Printing

    
    ======= input '1 || 0 && 0'
    result:     (1 || (0 && 0))
     - simplified: (0 && 0) -> false
     - simplified: (1 || (0 && 0)) -> true
    simplified: 1
    evaluated:  true
    
    ======= input '(a && b) ^ ((c && d) || (a && b))'
    result:     ((a && b) ^ ((c && d) || (a && b)))
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a && b) -> true
     - trace eval: c -> true
     - trace eval: d -> false
     - trace eval: (c && d) -> false
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a && b) -> true
     - trace eval: ((c && d) || (a && b)) -> true
     - trace eval: ((a && b) ^ ((c && d) || (a && b))) -> false
    evaluated:  false
    
    ======= input 'a && b ^ c && d || a && b'
    result:     (((a && b) ^ (c && d)) || (a && b))
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a && b) -> true
     - trace eval: c -> true
     - trace eval: d -> false
     - trace eval: (c && d) -> false
     - trace eval: ((a && b) ^ (c && d)) -> true
     - trace eval: (((a && b) ^ (c && d)) || (a && b)) -> true
    evaluated:  true
    
    ======= input 'a && b'
    result:     (a && b)
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a && b) -> true
    evaluated:  true
    
    ======= input '! a && b'
    result:     ((!a) && b)
     - trace eval: a -> true
     - trace eval: (!a) -> false
     - trace eval: ((!a) && b) -> false
    evaluated:  false
    
    ======= input '! (a && b)'
    result:     (!(a && b))
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a && b) -> true
     - trace eval: (!(a && b)) -> false
    evaluated:  false
    
    ======= input 'a || b || c'
    result:     (a || (b || c))
     - trace eval: a -> true
     - trace eval: (a || (b || c)) -> true
    evaluated:  true
    
    ======= input '(a && b)'
    result:     (a && b)
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a && b) -> true
    evaluated:  true
    
    ======= input 'a ^ b'
    result:     (a ^ b)
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a ^ b) -> false
    evaluated:  false
    
    ======= input 'a || b'
    result:     (a || b)
     - trace eval: a -> true
     - trace eval: (a || b) -> true
    evaluated:  true
    
    ======= input '(a) || (b)'
    result:     (a || b)
     - trace eval: a -> true
     - trace eval: (a || b) -> true
    evaluated:  true
    
    ======= input '((c && d) || (a && b))'
    result:     ((c && d) || (a && b))
     - trace eval: c -> true
     - trace eval: d -> false
     - trace eval: (c && d) -> false
     - trace eval: a -> true
     - trace eval: b -> true
     - trace eval: (a && b) -> true
     - trace eval: ((c && d) || (a && b)) -> true
    evaluated:  true