我需要解析一个相当简单的基于堆栈的语言,例如
1 2 add
3 1 sub
我在这里面临两个选择:
我从来没有使用过提升精神,但是从我所读到的内容(文档和示例)中我仍然无法决定使用提升精神来解决这个问题并解析这个问题是否过分。简单的语言,或者使用它而不是推出我自己的词法分析器和解析器(我认为不应该太难)的事情是有意义的。
使用boost精神为一个简单的基于堆栈的语言如上所述得到回报(因为我需要先学习它才能使用它)?
答案 0 :(得分:4)
在“详尽探索”类别中,让我使用Spirit Qi(v2.x)和X3
添加一些“动态解释”堆栈机器中显示了AST-ful方法(2阶段解析/执行)
这里语义动作必须使用凤凰演员“组合”:
<强> 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
注意:
sub
和div
中的订单或操作数是不。它需要一些先进的Phoenix-fu(http://www.boost.org/doc/libs/1_59_0/libs/phoenix/doc/html/phoenix/modules/scope/let.html)这个想法是一样的,但我们可以使用lambda来使用适当的功能组合。
我们甚至使用帮助器动态生成解析器表达式以及合适的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";
}
当然它打印相同的输出。
答案 1 :(得分:2)
在简单的c ++中,第一种方法可以非常简单:
int main() {
Machine<int> machine;
std::for_each(
std::istream_iterator<std::string> { std::cin },
{},
[&](auto& instr) { machine.process(instr); }
);
}
这利用了这样一个事实,即读取空白分隔的字符串作为“词法分析器”(tokenizer)就足够了。
现在,以最简单的方式实施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 强>
我已将其添加为separate answer
答案 2 :(得分:2)
我们见过pure standard library approach。
立即执行此execute
指令。
让我们创建一个构建AST(抽象语法树)的解析器。在我们简单的堆栈机器的情况下,它只是一个指令列表。我们称之为Tape
。
我仍然建议反对词法分析员。精灵v2支持Lexers(不是在X3中 - 但是?)。但在实践中,它们使问题复杂化,并且Spirit知道如何在不匹配的情况下回溯输入。所以你可以暂时匹配制作并尝试下一个,如果它不是正确的&#34;令牌&#34;。
这里的使用一个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_;
经验丰富的灵魂大师会在这里发现一个问题:opcode_
不是一个词汇,也不保证&#34;不同的标识符&#34;解析。例如。 "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- * en其他重复解析器表达式的区别。在PEG语法中,这些语法总是贪婪的,并且永远不会仅仅回溯一个元素(类似于&#34; Maximum Munch&#34;规则)。
我们不再有任何凌乱的switch
了。事实上,它隐藏在变体访问者中(见apply_visitor
)。
指令执行几乎未经修改,但我们将execute
重命名为operator()
,以便为访客概念建模。