【问题标题】:Using boost spirit for a stack based language将 boost 精神用于基于堆栈的语言
【发布时间】:2015-11-21 17:14:15
【问题描述】:

我需要解析一个相当简单的基于堆栈的语言,例如

1 2 add
3 1 sub

我在这里面临两个选择:

  1. 为标记编写我自己的词法分析器,然后继续解析它
  2. 使用助推精神

我从未使用过 boost spirit,但从我所阅读的内容(文档和示例)来看,我仍然无法确定使用 boost spirit 来 lex 和解析这种简单的语言是否过大,或者如果使用它而不是推出我自己的词法分析器和解析器是有意义的(我认为这不应该太难)。

将 boost spirit 用于上述基于堆栈的简单语言是否有回报(因为我需要先学习它才能使用它)?

【问题讨论】:

  • 今晚我会在livecoding.tv/sehe试试我的手
  • 以新手-中级精神使用者的身份发言——通常我不会从“在这里使用精神是否矫枉过正”的角度来考虑它,通常是我想制作的任何时候解析器的问题是“是否足够小且简单到可以使用精神”。它在语法将变得适度大,或需要大量语义动作,或预处理阶段或其他东西的时候,当精神开始失去吸引力时,imo。对于小事,一旦你习惯了,精神通常会很棒。
  • 完全同意,@ChrisBeck。所以当我谈到“矫枉过正”时,我主要指的是学习曲线。同时,找到我的X3版本here
  • @ChrisBeck This is a Qi sample 我不推荐 Spirit :) 我无法抗拒,只是为了对比这些方法。 (Disclaimer)
  • @ChrisBeck 并展示 X3+C++14 的优势:stackoverflow.com/a/33849279/85371

标签: c++ parsing boost boost-spirit


【解决方案1】:

在“详尽探索”类别中,让我添加一些使用 Spirit Qi (v2.x) 和 X3 的“即时解释”堆栈机器

请注意,the second answer 中显示了一种 AST-ful 方法(2 阶段解析/执行)

灵气中

这里的语义动作必须使用 Phoenix 演员“组合”:

Live On Coliru

#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/spirit/repository/include/qi_distinct.hpp>
#include <iostream>
#include <deque>

namespace qi = boost::spirit::qi;
namespace px = boost::phoenix;
namespace qr = boost::spirit::repository::qi;

using Stack = std::deque<int>;

namespace actors {

    struct pop {
        Stack& s_;

        Stack::value_type operator()() const {
            Stack::value_type v = s_.back();
            s_.pop_back();
            return v;
        }
    };

    struct push {
        Stack& s_;
        template <typename V> void operator()(V const& v) const {
            s_.push_back(v);
        }
    };

    struct dump {
        Stack& s_;
        void operator()() const {
            std::copy(s_.begin(), s_.end(), std::ostream_iterator<Stack::value_type>(std::cout, " "));
            std::cout << "\n";
        }
    };
}

int main() {
    Stack stack_;

    boost::spirit::istream_iterator f(std::cin >> std::noskipws), l; // Note the noskipws!
    bool ok;

    {
        using namespace qi;
        px::function<actors::pop>  pop_  = actors::pop{ stack_ };
        px::function<actors::push> push_ = actors::push{ stack_ };
        px::function<actors::dump> dump_ = actors::dump{ stack_ };

        ok = phrase_parse(f, l, 
               *(
                   eps [ dump_() ] >> 
                   (lexeme [ qr::distinct(graph) [
                          lit("add") [ push_(  pop_() + pop_()) ]
                        | lit("sub") [ push_(- pop_() + pop_()) ] // bit hackish
                        | lit("mul") [ push_(pop_() * pop_()) ]
                        | lit("div") [ push_(pop_() / pop_()) ] // TODO fix order
                        | lit("pop") [ pop_() ]
                      ] ] 
                    | int_ [ push_(_1) ]
                  )
                ), space);
    }

    if (!ok)
        std::cout << "Parse failed\n";

    if (f != l)
        std::cout << "Unparsed program data: '" << std::string(f,l) << "'\n";
}

打印

1 
1 2 
3 
3 3 
3 3 1 
3 2 
6 

注意事项:

在精神 X3 中

这个想法是一样的,但是我们可以使用 lambdas 来使用适当的函数组合。

我们甚至使用帮助器动态生成解析器表达式以及合适的binop

Live On Coliru

#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/include/support_istream_iterator.hpp>
#include <iostream>
#include <deque>
#include <cassert>

int main() {
    std::deque<int> stack_;

    boost::spirit::istream_iterator f(std::cin >> std::noskipws), l; // Note the noskipws!
    bool ok;

    {
        using namespace boost::spirit::x3;
        struct stack_tag {};

        auto binop = [](auto id, auto f) {
            auto apply = [=](auto& ctx) {
                auto& s = get<stack_tag>(ctx);
                assert(s.size()>=2);

                auto rhs = s.back(); s.pop_back();
                auto lhs = s.back(); s.pop_back();
                s.push_back(f(lhs, rhs));
            };

            return lexeme[as_parser(id) >> !graph] [apply];
        };

        auto push = [](auto& ctx) {
            auto& s = get<stack_tag>(ctx);
            s.push_back(_attr(ctx));
        };

        auto dump = [](auto& ctx) {
            auto& s = get<stack_tag>(ctx);
            std::copy(s.begin(), s.end(), std::ostream_iterator<int>(std::cout, " "));
            std::cout << "\n";
        };

        auto instr   = binop("add", [](auto a, auto b) { return a + b; })
                     | binop("sub", [](auto a, auto b) { return a - b; })
                     | binop("mul", [](auto a, auto b) { return a * b; })
                     | binop("div", [](auto a, auto b) { return a / b; })
                     | int_ [ push ]
                     ;

        auto parser  = skip(space) [ *(eps [ dump ] >> instr) >> eps/*post-skip*/ ];
        auto machine = with<stack_tag>(stack_) [parser];

        ok = parse(f, l, machine);
    }

    if (!ok)
        std::cout << "Parse failed\n";

    if (f != l)
        std::cout << "Unparsed program data: '" << std::string(f,l) << "'\n";
}

当然它会打印相同的输出。

  • 它没有 Qi 版本的缺点
  • 它的编译速度要快得多(2.9 秒对 9.2 秒!)
  • 注意:X3 需要 C++14

【讨论】:

  • 您认为Spirit.X3 本身有什么可能显着改进(可以完全保持C++14 语法)?
  • @Orient 你能改写一下吗?也许我们可以开始聊天(我经常在这里:chat.stackoverflow.com/rooms/10/loungec
  • “未解析的数据”有些误导:“未解析”有时用于表示“(漂亮)打印”。
【解决方案2】:

我们见过pure standard library approach

这个executed 立即指示。

让我们创建一个解析器来构建一个 AST(抽象语法树)。对于我们简单的堆栈机器,它只是一个指令列表。我们称之为Tape

使用 Boost Spirit

我仍然建议不要使用词法分析器。 Spirit v2 支持词法分析器(X3 还不支持?)。但在实践中,它们会使事情复杂化,Spirit 知道如何在不匹配的情况下回溯输入。因此,如果它不是正确的“令牌”,您可以试探性地匹配产品并尝试下一个。

使用 Spirit 语法应该是这样的:

Tape program;
boost::spirit::istream_iterator f(std::cin >> std::noskipws), l; // Note the noskipws!

if (parse(f, l, Parser::program, program)) {
    std::cout << "Parsed " << program.size() << " instructions\n";
} else {
    std::cout << "Parse failed\n";
}

现在,AST 类型是:

struct Add {};
struct Sub {};
struct Mul {};
struct Div {};
struct Pop {};

using Value = int;
using Instr = boost::variant<Add, Sub, Mul, Div, Pop, Value>;
using Tape  = std::vector<Instr>;

简单,对。

语法

在 X3 中,编写语法非常轻量级。自上而下:

auto instr   = opcode_ | int_;
auto program = skip(space) [*instr];

现在,我们要做的就是教它识别操作码。开始是:

struct opcodes : symbols<Instr> {
    opcodes() {
        this->add("add", Add{})("sub", Sub{})("mul", Mul{})("div", Div{})("pop", Pop{});
    }
} opcode_;

经验丰富的 Spirit 大师会在这里发现一个问题:opcode_ 不是词位,也不能保证“独特标识符”解析。例如。 "a dd" 将匹配 Add"additional" 也会匹配。

幸运的是,X3 让动态编写指令变得非常容易:

auto opcode_ = [] {
    struct opcodes : symbols<Instr> {
        opcodes() { this->add("add", Add{})("sub", Sub{})("mul", Mul{})("div", Div{})("pop", Pop{}); }
    } codes_;

    return lexeme[codes_ >> !graph];
}();

所以,现在两个洞都修好了。

完整演示

Live On Coliru

#include <iostream>
#include <deque>
#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/include/support_istream_iterator.hpp>

struct Add {};
struct Sub {};
struct Mul {};
struct Div {};
struct Pop {};

using Value = int;
using Instr = boost::variant<Add, Sub, Mul, Div, Pop, Value>;

struct Machine {
    using result_type = void;
    std::deque<Value> stack_;

    void operator()(Instr instr) {
        boost::apply_visitor(*this, instr);
    }

    void operator()(Add) {
        assert(stack_.size()>=2);
        auto op2 = stack_.back(); stack_.pop_back();
        auto op1 = stack_.back(); stack_.pop_back();
        stack_.push_back(op1 + op2);
    }

    void operator()(Sub) {
        assert(stack_.size()>=2);
        auto op2 = stack_.back(); stack_.pop_back();
        auto op1 = stack_.back(); stack_.pop_back();
        stack_.push_back(op1 - op2);
    }

    void operator()(Mul) {
        assert(stack_.size()>=2);
        auto op2 = stack_.back(); stack_.pop_back();
        auto op1 = stack_.back(); stack_.pop_back();
        stack_.push_back(op1 * op2);
    }

    void operator()(Div) {
        assert(stack_.size()>=2);
        auto op2 = stack_.back(); stack_.pop_back();
        auto op1 = stack_.back(); stack_.pop_back();
        assert(op2 != 0);
        stack_.push_back(op1 / op2);
    }

    void operator()(Value v) {
        stack_.push_back(v);
    }

    void operator()(Pop) {
        assert(stack_.size()>=1);
        stack_.pop_back();
    }

    void trace() const {
        using namespace std;
        // debug trace
        copy(stack_.begin(), stack_.end(), ostream_iterator<Value>(cout, " "));
        cout << "\n";
    }
};

using Tape = std::vector<Instr>;

namespace Parser {
    using namespace boost::spirit::x3;

    auto opcode_ = [] {
        struct opcodes : symbols<Instr> {
            opcodes() { this->add("add", Add{})("sub", Sub{})("mul", Mul{})("div", Div{})("pop", Pop{}); }
        } codes_;

        return lexeme[codes_ >> !graph];
    }();

    auto instr   = opcode_ | int_; // TODO
    auto program = skip(space) [*instr];
}

int main() {
    Tape program;
    boost::spirit::istream_iterator f(std::cin >> std::noskipws), l; // Note the noskipws!

    if (parse(f, l, Parser::program, program)) {
        std::cout << "Parsed " << program.size() << " instructions\n";
    } else {
        std::cout << "Parse failed\n";
    }

    if (f != l)
        std::cout << "Unparsed program data: '" << std::string(f,l) << "'\n";

    Machine machine;
    for (auto instr : program)
    {
        machine(instr);
        machine.trace();
    }
}

打印:

Parsed 7 instructions
1 
1 2 
3 
3 3 
3 3 1 
3 2 
6 

总结

这里的主要收获是:

  • 我们以声明方式定义我们的语法。对于更复杂的语法,这可能是一个巨大的优势
  • 我们可以免费回溯 - 因此无需提前分离令牌

    注意:这是一个 PEG 语法。回溯仅相当于尝试下一个同级替代方案或使当前规则失败(因此父规则可以尝试下一个同级替代方案)。

    这与正则表达式中的回溯有很大不同。您会注意到与 Kleene-* 其他重复解析器表达式的区别。在 PEG 语法中,这些总是贪婪的,并且永远不会只回溯单个元素(类似于“Maximum Munch”规则)。

  • 我们不再有任何乱七八糟的switch。事实上,它隐藏在 Variant Visitor 中(请参阅 apply_visitor)。

  • 指令执行几乎没有修改,但我们将execute 重命名为operator(),以便模拟访问者概念。

【讨论】:

  • 有趣。投反对票的人,投反对票的原因是什么?我们是来学习的。
【解决方案3】:

第一种方法在纯 c++ 中非常简单:

int main() {
    Machine<int> machine;

    std::for_each(
            std::istream_iterator<std::string> { std::cin },
            {},
            [&](auto& instr) { machine.process(instr); }
        );
}

这利用了读取空格分隔的字符串作为“词法分析器”(标记器)足够好的事实。

现在,以最简单的方式实现process

    static const char* opcodes[] = { "add", "sub", "mul", "div", "pop" };
    auto op = find(begin(opcodes), end(opcodes), instr);

    enum { add, sub, mul, div, pop, other };

    switch(op - opcodes) {
        case add: execute(Add{}); break;
        case sub: execute(Sub{}); break;
        case mul: execute(Mul{}); break;
        case div: execute(Div{}); break;
        case pop: execute(Pop{}); break;
        case other: {
            istringstream iss(instr);
            value_type v;
            if (iss >> v)
                execute(v);
            else
                throw runtime_error("Invalid instruction '" + instr + "'");
        }
    }

添加一些调试跟踪,我们得到程序“1 2 add 3 1 sub mul”的以下输出:

Executing 1: 1 
Executing 2: 1 2 
Executing add: 3 
Executing 3: 3 3 
Executing 1: 3 3 1 
Executing sub: 3 2 
Executing mul: 6 

Live On Coliru

使用 Boost Spirit

我已将其添加为separate answer

【讨论】:

    猜你喜欢
    • 2012-07-20
    • 2017-02-20
    • 2020-07-09
    • 1970-01-01
    • 1970-01-01
    • 2011-03-12
    • 2017-12-05
    • 1970-01-01
    • 2012-11-08
    相关资源
    最近更新 更多