用C ++创建自定义编译器的第一步之一就是开发编译器的预处理器引擎。 通常,C ++编译器具有单独的预处理引擎,可以完成以下四个基本任务:
定义和重新定义宏(# #define ,# #undef )。 支持头文件( #include指令)。 支持条件编译( #ifdef , #ifndef , #else , #endif )。 从输入源清单中删除所有注释。
通常,预处理器引擎的输出然后馈送到C ++词法分析器/解析器组合。 本文讨论使用Antlr的C ++预处理器的设计。 您应该对Antlr和自上而下的递归下降解析器的概念有所了解。 众所周知,本文讨论的所有代码都可在Antlr-2.7.2上运行,并使用gcc-3.4.4进行编译。
为预处理器创建一个解析器
将Antlr用作创建C ++编译器的首选解析器生成器工具的一个显着好处是不需要单独的预处理器引擎。 预处理器引擎可以集成为词法分析器的一部分。 要了解此策略,请回顾词法分析器和解析器通常是如何工作的。 词法分析器处理原始数据(在本例中为.h / .cpp文件),从该数据创建令牌,然后将令牌传递给解析器。 解析器依次处理令牌并针对语法进行验证,进行语义检查并最终生成汇编代码。
使用单个编译器和预处理器引擎的关键在于对词法分析器的适当修改。 Antlr词法分析器已扩展为可以预处理C ++源,并且仅将相关令牌传递给解析器。 解析器永远不会意识到预处理部分; 它仅与它从词法分析器收到的令牌有关。 考虑以下代码段:
#define USE_STREAMS
#ifdef USE_STREAMS
#include <iostream>
#else
# include <stdio.h>
#endif
假定此代码段已声明为C ++源文件的一部分。 词法分析器需要通过包含iostream标头将其找到的第一个令牌传递给解析器。 #define映射和#ifdef条件评估是词法分析器的附加职责。
通过定义新标记来增强Antlr词法分析器
使用C ++语言的词法分析器根据C ++语言标准定义令牌。 例如,您通常会找到表示平均lexer文件中定义的变量名称和语言关键字的标记。
要将预处理引擎与词法分析器结合使用,必须定义与预处理器构造相对应的新令牌类型。 在遇到这些构造时,词法分析器将采取必要的措施。 词法分析器还必须从源代码中删除注释。 在这种情况下,词法分析器在遇到注释标记时,需要忽略该标记并继续寻找下一个可用标记。 重要的是要注意,这些标记没有被定义为语言标准的一部分。 它们本质上是实现定义的令牌。
在词法分析器中定义新标记
该词法分析器必须包含以下标记以及语言要求的标记: COMMENT_TOKEN , INCLUDE_TOKEN , DEFINE_TOKEN , UNDEF_TOKEN , IFDEF_TOKEN , ELSE_TOKEN和ENDIF_TOKEN 。 本文讨论了一个原型词法分析器,它支持前面提到的预处理器宏以及整数和长数据类型的变量声明。 清单1显示了处理这些令牌的准系统词法分析器/解析器组合。
清单1.支持预处理器标记和long / int声明的准系统词法分析器/解析器
header {
#include "Main.hpp"
#include <string>
#include <iostream>
}
options {
language = Cpp;
}
class cppParser extends Parser;
start: ( declaration )+ ;
declaration: type a:IDENTIFIER (COMMA b:IDENTIFIER)* SEMI;
type: INT | LONG;
{
#include <fstream>
#include "cppParser.hpp"
}
class cppLexer extends Lexer;
options {
charVocabulary = '\3'..'\377';
k=5;
}
tokens {
INT="int";
LONG="long";
}
DEFINE_TOKEN: "#define" WS macroText:IDENTIFIER WS macroArgs:MACRO_TEXT;
UNDEF_TOKEN: "#undef" IDENTIFIER;
IFDEF_TOKEN: ("ifdef" | "#ifndef") WS IDENTIFIER;
ELSE_TOKEN: ("else" | "elsif" WS IDENTIFIER);
ENDIF_TOKEN: "endif";
INCLUDE_TOKEN: "#include" (WS)? f:STRING;
COMMENT_TOKEN:
(
"//" (~'\n')* '\n' { newline( ); }
|
"/*" (
{LA(2) != '/'}? '*' | '\n' { newline( ); } | ~('*'|'\n')
)*
"*/"
);
IDENTIFIER: ('a'..'z'|'A'..'Z'|'_')('a'..'z'|'A'..'Z'|'_'|'0'..'9')* ;
STRING: '"'! ( ~'"' )* '"'!
SEMI: ';';
COMMA: ',';
WS : ( ' ' | '\t' | '\f' | '\n' {newline();})+;
MACRO_TEXT: ( ~'\n' )* ;
定义词法分析器/解析器组合以去除注释
注释可以用C ++ //样式或C样式/* */多行注释样式编写。 词法分析器被扩展为包括COMMENT_TOKEN的规则。 遇到此令牌时,需要明确告知词法分析器不要将此令牌传递给解析器,而要跳过它并继续寻找下一个可用的令牌。 您可以使用$setType的$setType函数执行此操作。 您无需在解析器端添加支持,因为它永远不会传递注释令牌。 参见清单2。
清单2.定义词法分析器以从C / C ++代码中剥离注释
COMMENT_TOKEN:
(
"//" (~'\n')* '\n' { newline( ); }
|
"/*" (
{LA(2) != '/'}? '*' | '\n' { newline( ); } | ~('*'|'\n')
)*
"*/"
)
{ $setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP); };
该代码对于C ++样式的注释足够简单。 对于C样式的注释,请注意{LA(2) != '/'}? 匹配*前使用。 这意味着如果*后面有一个/ ,则此规则将不匹配,而词法分析器最终将匹配*/ 。 这是必需的,因为/* this is */ an invalid comment */是无效的C ++注释。 这也意味着词法分析器所需的最小提前量为2。LA LA(2)被称为语义谓词-提示词法分析器决定在输入流中接下来要寻找的字符。
包括文件处理
如前所述,解析器需要查看连续的令牌流。 因此,当词法分析器遇到包含文件时,需要切换流,并在遇到包含文件的末尾时切换回前一个流。 解析器不了解此流切换,实际上将包含的文件视为连续的令牌流。 您可以使用Antlr通过多种方式实现此行为。 有关详细讨论,请参见“ 相关主题”部分。
本文使用Antlr的TokenStreamSelector类。 与其使用清单1中的词法分析器初始化解析器,不如使用TokenStreamSelector对象初始化解析器。 在内部, TokenStreamSelector类维护一堆词法分析器。 当词法分析器遇到新的输入流时,它将创建一个新的词法分析器用于处理新的流,然后将新的词法分析器附加到tokenstreamselector对象,以便使用来获取所有将来的令牌(直到到达新流的末尾)。新的词法分析器类。 接下来,调用TokenStreamSelector类的retry方法,该类现在从新的输入流中获取令牌。 唯一要做的另一件事是在遇到包含文件中的EOF时切换回先前的输入流。 为了允许用户定义的遇到行尾的动作, uponEOF提供了预定义的例程。 您必须通过调用TokenStreamSelector::pop()修改此例程以切换到先前的输入流。 清单3显示了包含文件处理的代码段。
清单3.使用TokenStreamSelector对象初始化解析器
#include <iostream>
#include <fstream>
…
TokenStreamSelector selector;
cppParser* parser;
cppLexer* mainLexer;
int main(int argc,char** argv)
{
try {
std::ifstream inputstream("test.c", std::ifstream::in);
mainLexer = new cppLexer(inputstream);
// notify selector about starting lexer; name for convenience
selector.addInputStream(mainLexer, "main");
selector.select("main"); // start with main lexer
// Create parser attached to selector
parser = new cppParser (selector);
parser->setFilename("test.c");
parser->startRule();
}
catch (exception& e) {
cerr << "exception: " << e.what() << endl;
}
return 0;
}
清单4中描述了与词法分析器相关的更改。
清单4.在词法分析器中包含文件处理
class cppLexer extends Lexer;
…
{
public:
void uponEOF() {
if ( selector.getCurrentStream() != mainLexer ) {
selector.pop(); // return to old lexer/stream
selector.retry();
}
else {
ANTLR_USE_NAMESPACE(std)cout << "Hit EOF of main file\n" ;
}
}
}
INCLUDE_TOKEN: "#include" (WS)? f:STRING
{
ANTLR_USING_NAMESPACE(std)
// create lexer to handle include
string name = f->getText();
ifstream* input = new ifstream(name.c_str());
if (!*input) {
cerr << "cannot find file " << name << endl;
}
cppLexer* sublexer = new cppLexer (*input);
// make sure errors are reported in right file
sublexer->setFilename(name);
parser->setFilename(name);
// push the previous lexer stream and make sublexer current
selector.push(sublexer);
// ignore this token, re-look for token in the switched stream
selector.retry(); // throws TokenStreamRetryException
}
;
请注意, TokenStreamSelector对象是有意全局变量,因为它需要作为lexer uponEOF方法的一部分进行共享。 另外,在uponEOF方法文章中,您必须调用pop方法retry以便TokenStreamSelector再次在当前流中查找下一个标记。 请注意,此处描述的包含文件处理尚未完成,但取决于条件宏支持。 例如,如果一个源代码段包括一个内的文件#ifdef A .. #endif ,其中嵌段A没有定义,则对于处理INCLUDE_TOKEN被丢弃。 在处理宏和条件宏支持部分中对此进行了进一步说明。
处理宏
宏处理主要是在哈希表的帮助下完成的。 本文使用标准的STL哈希容器。 以最简单的形式,对宏的支持意味着支持#define A和#undef A类的构造。 您可以轻松地做到这一点,方法是在遇到#define将字符串“ A”压入哈希表,并在遇到#undef将其从哈希表中删除。 哈希表通常被定义为词法分析器的一部分。 另外,还要注意的是,令牌#define和#undef不能达到解析器-因为这个原因,你添加$setType(ANTLR_USE_NAMESPACE(Antlr)Token::SKIP); 作为相应令牌处理的一部分。 清单5显示了这种行为。
清单5.支持#define和#undef
class cppLexer extends Lexer;
…
{
bool processingMacro;
std::map<std::string, std::string> macroDefns;
public:
void uponEOF() {
… // Code for include processing
}
}
…
DEFINE_TOKEN: "#define" {processingMacro=true;}
WS macroText:IDENTIFIER WS macroArgs:MACRO_TEXT
{
macroDefns[macroText->getText()] = string(macroArgs->getText());
$setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
} ;
UNDEF_TOKEN: "#undef" WS macroText:IDENTIFIER
{
macroDefns.erase(macroText->getText());
$setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
};
MACRO_TEXT: {processingMacro}? ( ~'\n' )* {processingMacro=false;};
条件宏支持
广义地说,支持条件宏意味着您需要根据条件来考虑哪些令牌到达解析器。 例如,查看以下代码片段:
#define A
#ifdef A
#ifdef B
int c;
#endif
int d;
#endif
在遇到第一个#ifdef的令牌时,您必须根据先前是否已定义A来决定是否将遇到的后续令牌传递到解析器。 在此阶段,您将使用之前定义的哈希表。
接下来,您必须考虑支持嵌套的#ifdef块。 因为您需要在遇到每个#ifdef检查路径条件,所以为路径条件维护堆栈是合乎逻辑的。 每个#ifdef / #elsif将路径条件添加到堆栈头; 匹配的#endif将从堆栈中删除路径条件。
在看到更详细地解释该概念的代码之前,您需要了解Antlr lexer类中的另一个重要方法: nextToken ,解析器连续调用该方法以从输入流中检索下一个令牌。 您不能直接修改此例程,因此最佳方法是从cppLexer类派生一个类,然后根据路径条件重新定义nextToken方法。 如果路径条件为true,则例程将下一个可用令牌返回到解析器;否则,例程返回解析器。 否则,它将继续直到在令牌流中找到匹配的#endif为止。
清单6显示了派生的lexer类的源。 代码中不应直接实例化cppLexer ; 解析器应使用派生的lexer类对象的副本进行初始化。
清单6.定义一个新的lexer类
class cppAdvancedLexer : public cppLexer
{
public:
cppAdvancedLexer(ANTLR_USE_NAMESPACE(std)istream& in) : cppLexer(in) { }
RefToken nextToken()
{
// keep looking for a token until you don't
// get a retry exception
for (;;) {
try {
RefToken _next = cppLexer::nextToken();
if (processToken.empty() || processToken.top()) // defined in cppLexer
return _next;
}
catch (TokenStreamRetryException& /*r*/) {
// just retry "forever"
}
}
}
};
清单7中显示了与词法分析器相关的更改。
清单7.词法分析器中的条件宏支持
{
public:
std::stack<bool> processToken;
std::stack<bool> pathProcessed;
std::map<std::string, std::list<std::string> > macroDefns;
void uponEOF() {
… // Code for include processing defined earlier
}
}
IFDEF_TOKEN {bool negate = false; } :
("#ifdef" | "#ifndef" {negate = true;} ) WS macroText:IDENTIFIER
{
bool macroAlreadyDefined = false;
if (macroDefns.find(macroText->getText()) != macroDefns.end())
macroAlreadyDefined = true;
processToken.push(negate? !macroAlreadyDefined : macroAlreadyDefined);
pathProcessed(processToken.top());
$setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
}
;
ELSE_TOKEN: ("#else"
{
bool& pathCondition = processToken.top();
pathCondition = !processToken.top(); // no other path is true
}
|
"#elsif" WS macroText:IDENTIFIER
{
if (!processToken.top() && !pathProcessed.top()) {
if (macroDefns.find(macroText->getText()) != macroDefns.end()) {
processToken.push(true);
pathProcessed.push(true);
}
}
else {
bool& condition = pathProcessed.top();
condition = false;
}
)
{
$setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
}
;
ENDIF_TOKEN: "#endif"
{
processToken.pop();
pathProcessed.pop();
$setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
};
查看代码,您会看到processToken被定义为一堆布尔值,用于存储路径条件。 遇到#ifdef ,如果代码已经在真实路径中,则它将测试路径条件是否有效。
支持将表达式作为宏的一部分
您可以使用宏来表示包含数字,标识符,数学运算符甚至函数调用的复杂表达式。 为此,必须使用macroArgs字符串替换后处理的C ++代码中每次出现的宏定义。 上下文确定此替换在语法上是否有效。 例如,考虑这种情况: #define A b, c 。 在行的某处,您有int A; ,这是有效的C ++; 但是switch(A)显然是无效的C ++。 您需要逐次分析与字符串关联的流,并让语法规则确定上下文的语法有效性。
此方法的代码如下:
在cppAdvancedLexer::nextToken捕获与macroText标识符相对应的令牌。 从哈希表中检索macroArgs文本。 为macroArgs创建一个istream ,并为解析该流的词法分析器创建一个新副本。 将此词法分析器附加到TokenStreamSelector对象,然后调用retry -这将从刚刚创建的新流中获取令牌。
该uponEOF中提供了方法cppLexer ,这需要在遇到流的末尾护理切换上下文。 清单8说明了这一讨论。
清单8.修改lexer类的nextToken方法以进行宏替换
RefToken cppAdvancedLexer::nextToken()
{
// keep looking for a token until you don't
// get a retry exception
for (;;) {
try {
RefToken _next = cppLexer::nextToken();
map<string, string>::iterator defI = macroDefns.find(_next->getText());
if (defI != macroDefns.end() && defI->second != "")
{
std::stringstream macroStream;
cppAdvancedLexer* macrolexer = new cppAdvancedLexer(macroStream);
macrolexer->setFilename(this->getFilename());
selector.push(macrolexer);
selector.retry();
}
if (processToken.empty() || processToken.top())
return _next;
}
catch (TokenStreamRetryException& /*r*/) {
// just retry "forever"
}
}
结论
本文讨论了可以使用Antlr创建C ++预处理程序的基本方法。 开发成熟的C ++预处理器超出了本文的范围。 尽管本文没有涉及潜在的错误和错误处理策略,但现实世界中的预处理器必须考虑这些因素。 例如,条件宏处理中令牌的顺序很重要,您必须采取步骤以确保正确处理令牌。
翻译自: https://www.ibm.com/developerworks/aix/library/au-c_plusplus_antlr/index.html