Boost Spirit 解析器框架介绍






4.79/5 (23投票s)
2004 年 10 月 9 日
8分钟阅读

306694

2761
使用 boost::spirit 库生成解析器的基础介绍。
引言
这是关于 boost::spirit 解析器框架使用情况的二部分文章中的第一篇。Spirit 是我所见过的最好的完全面向对象的解析器,它允许用户通过高度面向对象的代码快速创建功能齐全的解析器。这第一篇文章旨在向程序员介绍编写足够的代码来成功解析一些简单输入。我将不涉及任何语义动作。下一篇文章将实际整合一些语义动作,并创建一个功能齐全的模块化算术计算器。
背景
多年来,我似乎注定要编写大量的解析器。我的第一个解析器是一个编程语言解析器,在 8 位机器上直接用机器码编写,当时我大约 16 岁。从那时起,我使用了 Lex 和 Yacc;我编写了自己的通用 LALR 解析器;进行了自定义解析器;几乎遇到了所有可能想象到的问题。我一直喜欢 Lex 和 Yacc,但在编写面向对象代码时,我从未对它们感到满意。在我看来,即使是这些工具所谓的面向对象变体也不完全令人满意。去年,我决定研究一下 Spirit。Spirit 刚刚被添加到 Boost 库中,我非常喜欢 Boost 库,并决定尝试一下。起初,我写了一个函数式编程语言,但在文章中,我使用的是一个模块化算术计算器。我写这个模块化算术计算器是为了帮助我完成数论专业的硕士课程,但我无意解释什么是模块化算术,所以如果你无法弄清楚除法返回的是什么,不要期望这篇文章能回答这个问题!
Spirit 是一个完全面向对象的 LL 解析器和词法分析器。由于词法分析器实际上只是一个优化用于将数据处理成令牌流的解析器,因此 Spirit 对过程的两个部分进行几乎相同的处理。本质上,在 Spirit 中,解析器是一个知道如何解析特定字符串的小对象。例如,real_p
实际上是一个解析实数的解析器。这是 Spirit 内置的一整套解析器之一。其他示例包括:ch_p('+')
或 alpha_p
,分别匹配单个加号字符或单个字母字符。
Spirit 使用各种重载运算符,允许程序员使用一种与扩展巴科斯范式 (EBNF) 非常相似的语法将各种解析器链接在一起。EBNF 是 BNF 的一个简单扩展,它引入了像 Kleene 星号(熟悉正则表达式的人应该知道,它表示前一个实体零次或多次)这样的概念。使用 Kleene 星号可以替换 BNF 中常见的许多递归定义,用迭代定义来代替。
请注意,此代码只能在 Visual C++ 7.1 或更高版本上编译,并且需要安装最新的 Boost 库,并将其添加到您的包含路径中。
编写解析器
使用 Spirit 编写解析器再简单不过了。必须通过继承 boost::spirit::grammar
来描述语法,并通过调用 boost::spirit::parse
函数来执行解析。总的来说,就是这样!
语法必须遵循特定的布局。语法本身继承自 boost::spirit::grammar
模板,并使用“好奇的递归模板模式”。在语法内部,您必须定义一个名为 definition
的嵌套 struct
,它将包含解析器的定义,以及一个名为 start()
的函数,该函数返回起始规则。以下代码片段定义了我在模块化算术计算器中使用的解析器。该语法包含两个语义动作,我要求您暂时忽略它们。语义动作的概念将在我的下一篇文章中介绍。
struct Syntax : public boost::spirit::grammar<Syntax> { public: Syntax( CParser &parser ); virtual ~Syntax(); template <typename ScannerT> struct definition { public: definition( Syntax const &self ) { integer = lexeme_d[ (+digit_p) ] ; factor = integer | vars | '(' >> expression >> ')' | ( '-' >> factor ) | ( '+' >> factor ) ; term = factor >> *( ( '*' >> factor) | ( '/' >> factor ) ) ; expression = term >> *( ( '+' >> term ) | ( '-' >> term ) ) ; assignment = vars >> '=' >> expression ; var_decl = lexeme_d [ ( ( alpha_p >> *( alnum_p | '_' ) ) - vars )[vars.add] ] ; declaration = "int" >> var_decl >> *( ',' >> var_decl ) ; baseExpression = str_p( "exit" )[*self.m_finishedProcessing] | str_p( "mod" ) >> integer | declaration | assignment | '?' >> expression ; } boost::spirit::symbols<int> vars; boost::spirit::rule<ScannerT> integer, factor, term, expression, assignment, var_decl, declaration, baseExpression; const boost::spirit::rule<ScannerT> &start() const { return baseExpression; } }; friend struct definition; private: Private::FinishedProcessing *m_finishedProcessing; };
正如您所看到的,definition
struct
构造函数实际上将语法描述为一系列 EBNF 规则。如果我们仔细查看其中的几个规则,term
规则由一个 factor
后面跟着零个或多个(Kleene 星号,*)'*' 副本,后面跟着一个 factor 或一个 '/',最后再跟着一个 factor 组成。它对应于以下 EBNF 规则
term ::= factor ( ( '*' factor ) | ( '/' factor ) )*
另一个需要仔细看看的规则是 integer
规则。它使用 lexeme_d
指令告诉解析器不要跳过空格。然后,+digit_p
是一个匹配一个或多个数字字符的解析器。
语义动作包含在 var_decl
和 baseExpression
规则中,代码分别为:[vars.add]
和 [*self.m_finishedProcessing]
。如前所述,我不会详细解释这些,尽管其中一个用于实现变量的符号表,另一个允许您键入 exit 来关闭计算器。
最后需要注意的一点是,在 definition
构造函数中定义的规则必须在类中实际声明,它们的类型为 boost::spirit::rule<ScannerT>
。另外,start 函数将简单地返回入口点规则,在本例中为 baseExpression
。
运行解析器
运行解析器非常简单。如果我们定义了一个名为 Syntax
的语法,那么在存储在 std::string line
中的文本行上,可以使用以下方式运行一个基本解析器:
boost::spirit::parse_info<> info; Syntax parser; info = boost::spirit::parse( line.c_str(), parser, boost::spirit::space_p );
结果类 boost::spirit::parse_info
告诉您解析器是否成功解析了所有输入,如果未成功,则会指示失败发生的位置。最后一个参数是一个跳过解析器,它告诉解析器跳过输入中的空格。parse
函数接受一个字符串,或者一对迭代器(begin 和 end),从而允许极大的使用灵活性。
Spirit 的问题
我不能说使用 Spirit 解析器进行编程没有给我带来一些问题。我在这里解释其中一些问题,以便让您了解如果您决定使用这个库可能会遇到什么。
编译错误
使用 Spirit 时出现的编译错误通常会延伸到数页难以理解的垃圾信息。不幸的是,Spirit 库使用的复杂模板化代码会导致 Microsoft 编译器将错误报告为每个模板化代码层被解包。为了调试编译问题,您通常需要仔细地逐步检查报告的每一层错误,最终发现是遗漏了一个分号。此外,在极端情况下,您可能会遇到一个导致 C1001 内部编译器错误的错误,而不知道发生了什么。我个人曾见过可怕的 C1001 错误,仅仅是因为我忘记声明了一个函子。哎哟!Spirit 的作者正在将 C++ 的能力推向极限,而不幸的是,编译器作者才刚刚开始实现许多这些功能。Spirit 在 Visual C++ 7.0 上无法编译,因为它需要仅在 Visual C++ 7.1 中引入的部分模板特化功能(以及其他功能)。不要认为其他编译器会更容易。GCC 报告的错误可能同样难以理解。不要因此而不使用 Spirit,我认为您可以逐渐习惯这些错误并与之共存,但一开始不要期望一帆风顺。
编译时间
同样,由于 Spirit 将编译器推向极限,预计编译时间会很慢,尤其是在复杂的语法下。请非常小心地将您的解析器包含在太多的 CPP 文件中,这样这个问题是可以管理的。Spirit 文档涵盖了各种技术,可以在情况失控时尝试简化您的解析器并缩短编译时间。
最小化重建
据我所知,Visual Studio 中的最小化重建选项与 Spirit 不兼容。我花了很多时间试图调试一个不如我预期的解析器,结果发现它没有重新编译,因为最小化重建框架未能检测到模板定义深度中的某个更改。一旦您将 Spirit 集成到您的应用程序中,就尽快关闭最小化重建,这样您应该就不会遇到问题了。
难以理解的语法
乍一看,Spirit 库中所有复杂的模板和大量的操作符重载可能会使语法难以理解。我的建议是,至少一开始,将所有通用的“管道”代码视为样板代码,而只专注于主要的解析器代码。复习一下您的扩展巴科斯范式 (EBNF),您会发现一切看起来都相似。到那时,一切都会开始变得有意义。
结论
boost::spirit
库包含一套令人印象深刻的类,可以使面向对象的解析器创建既快速又简洁。它具有易于使用的优点,无需预处理语法文件或使用外部工具自动生成代码。它也有其问题。为了实现该库,程序员使用了许多 ANSI 标准 C++ 的前沿功能,其中许多功能才刚刚开始在主流编译器中出现。因此,程序员可能会遇到一些问题,最显著的是难以理解的错误消息和缓慢的编译时间。尽管如此,我的建议是尝试使用它,解析器的清晰定义使其成为编写简单解析器的可靠选择。
历史
2004 年 10 月 9 日:版本 1 发布。