Search code examples
c++abstract-syntax-treeboost-spirit

Virtual classes as AST nodes with Spirit


i was working on an interpreter for a language with a friend, and we started with a decision I'm guessing wasn't that wise: we made all the elements for execution first (practically a tree made of different classes); but now looking at boost examples i get a lot confused about how to merge the two. I know what to start from (the grammar), i know what to reach (instantiated classes owning each other), i don't know how to reach it.

We started with expressions without variables, hence we looked at spirit calculator examples; but i don't understand when to instantiate elements.

Example of expression items:

namespace exp
{
class op
    {
    private:
    public:
        virtual double exec(function_scope &fs);

    };

class operand : public op
    {
    private:
        double value;

    public:
        operand(double value);
        double exec(function_scope &fs);
    };

class op_bin : public op
    {
    private:
    public:
        op * ll;
        op* rr;
        op_bin(op* ll, op* rr);
        ~op_bin();
    };

namespace bin
    {
    class sum : public op_bin
        {
        public:
            sum(op* ll, op* rr);
            double exec(function_scope &fs);
        };
    }
}

Ignore the exec function, it's used at runtime.

For example the code 5 + (2 + 1) should result in a final equivalent of:

new exp::bin::sum(new exp::operand(5), new exp::bin::sum(new exp::operand(2), new exp::operand(1))

Once i understand how to do that I've practically done.


Solution

  • Well, I was going to write what's wrong with your question, but instead I went to prove myself that it is not that hard to make what you want.

    Few keypoints:

    • I slightly modified, renamed and extended your ast to make it work and to actually show something.
    • Spirit rules for some reason make copy of an attribute (I think it is a bug), so I workarounded this issue for unique_ptr with a trait. (fixed in 1.70)
    • I am not sure if x3::omit is actually required there (you can remove all except the last and it will compile), but it looks like it is an another bug in Spirit.
    • make_node looks unreliable and may broke in surprising ways, you can split it into separate unary/binary node creators if you wish.
    • At some point you will want to use stateful allocator for your ast nodes creation, it should be very simple by injecting allocator into the parser context. I am leaving it for you as an exercise.

    The parser:

    #include <boost/spirit/home/x3.hpp>
    #include <memory>
    #include <iostream>
    
    namespace ast
    {
    
    class expression
    {
    protected:
        expression() = default;
    public:
        virtual ~expression() = default;
        expression(expression&& other) = delete;
        expression& operator=(expression&& other) = delete;
    
        virtual void print(std::ostream&) const = 0;
    
        friend std::ostream& operator<<(std::ostream& os, expression const& node)
        {
            node.print(os);
            return os;
        }
    };
    
    class operand : public expression
    {
        double value_;
    
    public:
        constexpr operand(double value) : value_{value} {}
        void print(std::ostream& os) const override { os << value_; }
    };
    
    class op_bin : public expression
    {
    protected:
        std::unique_ptr<expression> left_, right_;
    
    public:
        op_bin(std::unique_ptr<expression> left, std::unique_ptr<expression> right)
          : left_{ std::move(left) }, right_{ std::move(right) }
        {}
    
        op_bin(expression * left, expression * right)
            : left_{ left }, right_{ right }
        {}
    };
    
    class plus : public op_bin
    {
    public:
        using op_bin::op_bin;
        void print(std::ostream& os) const override
        { os << '(' << *left_ << " + " << *right_ << ')'; }
    };
    
    class minus : public op_bin
    {
    public:
        using op_bin::op_bin;
        void print(std::ostream& os) const override
        { os << '(' << *left_ << " - " << *right_ << ')'; }
    };
    
    class mul : public op_bin
    {
    public:
        using op_bin::op_bin;
        void print(std::ostream& os) const override
        { os << '(' << *left_ << " * " << *right_ << ')'; }
    };
    
    class div : public op_bin
    {
    public:
        using op_bin::op_bin;
        void print(std::ostream& os) const override
        { os << '(' << *left_ << " / " << *right_ << ')'; }
    };
    
    } // namespace ast
    
    namespace grammar
    {
    
    namespace x3 = boost::spirit::x3;
    
    template <typename T>
    struct make_node_
    {
        template <typename Context>
        void operator()(Context const& ctx) const
        {
            if constexpr (std::is_convertible_v<decltype(x3::_attr(ctx)), T>) {
                x3::_val(ctx) = std::make_unique<T>(std::move(x3::_attr(ctx)));
            }
            else {
                x3::_val(ctx) = std::make_unique<T>(std::move(x3::_val(ctx)), std::move(x3::_attr(ctx)));
            }
        }
    };
    
    template <typename T>
    constexpr make_node_<T> make_node{};
    
    using x3::double_;
    using x3::char_;
    
    x3::rule<class expression_r, std::unique_ptr<ast::expression>, true> const expression;
    x3::rule<class prec1_r, std::unique_ptr<ast::expression>, true> const prec1;
    x3::rule<class prec0_r, std::unique_ptr<ast::expression>, true> const prec0;
    
    auto const expression_def =
        prec1
        >> *(   x3::omit[('+' > prec1)[make_node<ast::plus>]]
            |   x3::omit[('-' > prec1)[make_node<ast::minus>]]
            )
        ;
    
    auto const prec1_def =
        prec0
        >> *(   x3::omit[('*' > prec0)[make_node<ast::mul>]]
            |   x3::omit[('/' > prec0)[make_node<ast::div>]]
            )
        ;
    
    auto const prec0_def =
            x3::omit[double_[make_node<ast::operand>]]
        |   '(' > expression > ')'
        ;
    
    BOOST_SPIRIT_DEFINE(
        expression
      , prec1
      , prec0
    );
    
    } // namespace grammar
    
    #if BOOST_VERSION < 107000
    namespace boost::spirit::x3::traits {
    
    template <typename Attribute>
    struct make_attribute<std::unique_ptr<Attribute>, std::unique_ptr<Attribute>>
      : make_attribute_base<std::unique_ptr<Attribute>>
    {
        typedef std::unique_ptr<Attribute>& type;
        typedef std::unique_ptr<Attribute>& value_type;
    };
    
    } // namespace boost::spirit::x3::traits
    #endif
    
    int main()
    {
        namespace x3 = boost::spirit::x3;
    
        std::string s = "1 + 2 * (3 - 4) / 5";
        std::unique_ptr<ast::expression> expr;
        if (auto iter = s.cbegin(); !phrase_parse(iter, s.cend(), grammar::expression, x3::space, expr)) {
            std::cout << "parsing failed";
        }
        else {
            if (iter != s.cend())
                std::cout << "partially parsed\n";
            std::cout << *expr << '\n';
        }
    }
    

    Output:

    (1 + ((2 * (3 - 4)) / 5))