有哪些关于如何使用C ++编写词法分析器的好资源(书籍,教程,文档),有哪些好的技术和实践?
我在互联网上看过,每个人都说要使用像lex这样的词法生成器。我不想这样做,我想手动编写一个词法分析器。
有哪些关于如何使用C ++编写词法分析器的好资源(书籍,教程,文档),有哪些好的技术和实践?
我在互联网上看过,每个人都说要使用像lex这样的词法生成器。我不想这样做,我想手动编写一个词法分析器。
Answers:
请记住,每个有限状态机都对应一个正则表达式,该正则表达式对应于使用if
和while
语句的结构化程序。
因此,例如,要识别整数,您可以使用状态机:
0: digit -> 1
1: digit -> 1
或正则表达式:
digit digit*
或结构化代码:
if (isdigit(*pc)){
while(isdigit(*pc)){
pc++;
}
}
就个人而言,我总是使用后者来编写词法分析器,因为恕我直言,它并不清楚,也没有更快的方法。
*pc
,对吗?喜欢while(isdigit(*pc)) { value += pc; pc++; }
。然后,}
您将值转换为数字并将其分配给令牌。
n = n * 10 + (*pc++ - '0');
。对于浮点数和“ e”表示法,它有点复杂,但还不错。我确定可以通过将字符打包到缓冲区中并调用atof
或其他方式来保存一些代码。它不会运行得更快。
词法分析器是有限状态机。因此,它们可以由任何通用的FSM库构造。但是,出于自己的教育目的,我使用表达式模板编写了自己的模板。这是我的词法分析器:
static const std::unordered_map<Unicode::String, Wide::Lexer::TokenType> reserved_words(
[]() -> std::unordered_map<Unicode::String, Wide::Lexer::TokenType>
{
// Maps reserved words to TokenType enumerated values
std::unordered_map<Unicode::String, Wide::Lexer::TokenType> result;
// RESERVED WORD
result[L"dynamic_cast"] = Wide::Lexer::TokenType::DynamicCast;
result[L"for"] = Wide::Lexer::TokenType::For;
result[L"while"] = Wide::Lexer::TokenType::While;
result[L"do"] = Wide::Lexer::TokenType::Do;
result[L"continue"] = Wide::Lexer::TokenType::Continue;
result[L"auto"] = Wide::Lexer::TokenType::Auto;
result[L"break"] = Wide::Lexer::TokenType::Break;
result[L"type"] = Wide::Lexer::TokenType::Type;
result[L"switch"] = Wide::Lexer::TokenType::Switch;
result[L"case"] = Wide::Lexer::TokenType::Case;
result[L"default"] = Wide::Lexer::TokenType::Default;
result[L"try"] = Wide::Lexer::TokenType::Try;
result[L"catch"] = Wide::Lexer::TokenType::Catch;
result[L"return"] = Wide::Lexer::TokenType::Return;
result[L"static"] = Wide::Lexer::TokenType::Static;
result[L"if"] = Wide::Lexer::TokenType::If;
result[L"else"] = Wide::Lexer::TokenType::Else;
result[L"decltype"] = Wide::Lexer::TokenType::Decltype;
result[L"partial"] = Wide::Lexer::TokenType::Partial;
result[L"using"] = Wide::Lexer::TokenType::Using;
result[L"true"] = Wide::Lexer::TokenType::True;
result[L"false"] = Wide::Lexer::TokenType::False;
result[L"null"] = Wide::Lexer::TokenType::Null;
result[L"int"] = Wide::Lexer::TokenType::Int;
result[L"long"] = Wide::Lexer::TokenType::Long;
result[L"short"] = Wide::Lexer::TokenType::Short;
result[L"module"] = Wide::Lexer::TokenType::Module;
result[L"dynamic"] = Wide::Lexer::TokenType::Dynamic;
result[L"reinterpret_cast"] = Wide::Lexer::TokenType::ReinterpretCast;
result[L"static_cast"] = Wide::Lexer::TokenType::StaticCast;
result[L"enum"] = Wide::Lexer::TokenType::Enum;
result[L"operator"] = Wide::Lexer::TokenType::Operator;
result[L"throw"] = Wide::Lexer::TokenType::Throw;
result[L"public"] = Wide::Lexer::TokenType::Public;
result[L"private"] = Wide::Lexer::TokenType::Private;
result[L"protected"] = Wide::Lexer::TokenType::Protected;
result[L"friend"] = Wide::Lexer::TokenType::Friend;
result[L"this"] = Wide::Lexer::TokenType::This;
return result;
}()
);
std::vector<Wide::Lexer::Token*> Lexer::Context::operator()(Unicode::String* filename, Memory::Arena& arena) {
Wide::IO::TextInputFileOpenArguments args;
args.encoding = Wide::IO::Encoding::UTF16;
args.mode = Wide::IO::OpenMode::OpenExisting;
args.path = *filename;
auto str = arena.Allocate<Unicode::String>(args().AsString());
const wchar_t* begin = str->c_str();
const wchar_t* end = str->c_str() + str->size();
int line = 1;
int column = 1;
std::vector<Token*> tokens;
// Some variables we'll need for semantic actions
Wide::Lexer::TokenType type;
auto multi_line_comment
= MakeEquality(L'/')
>> MakeEquality(L'*')
>> *( !(MakeEquality(L'*') >> MakeEquality(L'/')) >> eps)
>> eps >> eps;
auto single_line_comment
= MakeEquality(L'/')
>> MakeEquality(L'/')
>> *( !MakeEquality(L'\n') >> eps);
auto punctuation
= MakeEquality(L',')[[&]{ type = Wide::Lexer::TokenType::Comma; }]
|| MakeEquality(L';')[[&]{ type = Wide::Lexer::TokenType::Semicolon; }]
|| MakeEquality(L'~')[[&]{ type = Wide::Lexer::TokenType::BinaryNOT; }]
|| MakeEquality(L'(')[[&]{ type = Wide::Lexer::TokenType::OpenBracket; }]
|| MakeEquality(L')')[[&]{ type = Wide::Lexer::TokenType::CloseBracket; }]
|| MakeEquality(L'[')[[&]{ type = Wide::Lexer::TokenType::OpenSquareBracket; }]
|| MakeEquality(L']')[[&]{ type = Wide::Lexer::TokenType::CloseSquareBracket; }]
|| MakeEquality(L'{')[[&]{ type = Wide::Lexer::TokenType::OpenCurlyBracket; }]
|| MakeEquality(L'}')[[&]{ type = Wide::Lexer::TokenType::CloseCurlyBracket; }]
|| MakeEquality(L'>') >> (
MakeEquality(L'>') >> (
MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::RightShiftEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::RightShift; }])
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::GreaterThanOrEqualTo; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::GreaterThan; }])
|| MakeEquality(L'<') >> (
MakeEquality(L'<') >> (
MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::LeftShiftEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::LeftShift; }] )
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::LessThanOrEqualTo; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::LessThan; }])
|| MakeEquality(L'-') >> (
MakeEquality(L'-')[[&]{ type = Wide::Lexer::TokenType::Decrement; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::MinusEquals; }]
|| MakeEquality(L'>')[[&]{ type = Wide::Lexer::TokenType::PointerAccess; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Minus; }])
|| MakeEquality(L'.')
>> (MakeEquality(L'.') >> MakeEquality(L'.')[[&]{ type = Wide::Lexer::TokenType::Ellipsis; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Dot; }])
|| MakeEquality(L'+') >> (
MakeEquality(L'+')[[&]{ type = Wide::Lexer::TokenType::Increment; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::PlusEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Plus; }])
|| MakeEquality(L'&') >> (
MakeEquality(L'&')[[&]{ type = Wide::Lexer::TokenType::LogicalAnd; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::BinaryANDEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::BinaryAND; }])
|| MakeEquality(L'|') >> (
MakeEquality(L'|')[[&]{ type = Wide::Lexer::TokenType::LogicalOr; }]
|| MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::BinaryOREquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::BinaryOR; }])
|| MakeEquality(L'*') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::MulEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Multiply; }])
|| MakeEquality(L'%') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::ModulusEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Modulus; }])
|| MakeEquality(L'=') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::EqualTo; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Assignment; }])
|| MakeEquality(L'!') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::NotEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::LogicalNOT; }])
|| MakeEquality(L'/') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::DivEquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Divide; }])
|| MakeEquality(L'^') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::BinaryXOREquals; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::BinaryXOR; }])
|| MakeEquality(L':') >> (MakeEquality(L'=')[[&]{ type = Wide::Lexer::TokenType::VarAssign; }]
|| opt[[&]{ type = Wide::Lexer::TokenType::Colon; }]);
auto string
= L'"' >> *( L'\\' >> MakeEquality(L'"') >> eps || !MakeEquality(L'"') >> eps) >> eps;
auto character
= L'\'' >> *( L'\\' >> MakeEquality(L'\'') >> eps || !MakeEquality(L'\'') >> eps);
auto digit
= MakeRange(L'0', L'9');
auto letter
= MakeRange(L'a', L'z') || MakeRange(L'A', L'Z');
auto number
= +digit >> ((L'.' >> +digit) || opt);
auto new_line
= MakeEquality(L'\n')[ [&] { line++; column = 0; } ];
auto whitespace
= MakeEquality(L' ')
|| L'\t'
|| new_line
|| L'\n'
|| L'\r'
|| multi_line_comment
|| single_line_comment;
auto identifier
= (letter || L'_') >> *(letter || digit || (L'_'));
//= *( !(punctuation || string || character || whitespace) >> eps );
bool skip = false;
auto lexer
= whitespace[ [&]{ skip = true; } ] // Do not produce a token for whitespace or comments. Just continue on.
|| punctuation[ [&]{ skip = false; } ] // Type set by individual punctuation
|| string[ [&]{ skip = false; type = Wide::Lexer::TokenType::String; } ]
|| character[ [&]{ skip = false; type = Wide::Lexer::TokenType::Character; } ]
|| number[ [&]{ skip = false; type = Wide::Lexer::TokenType::Number; } ]
|| identifier[ [&]{ skip = false; type = Wide::Lexer::TokenType::Identifier; } ];
auto current = begin;
while(current != end) {
if (!lexer(current, end)) {
throw std::runtime_error("Failed to lex input.");
}
column += (current - begin);
if (skip) {
begin = current;
continue;
}
Token t(begin, current);
t.columnbegin = column - (current - begin);
t.columnend = column;
t.file = filename;
t.line = line;
if (type == Wide::Lexer::TokenType::Identifier) { // check for reserved word
if (reserved_words.find(t.Codepoints()) != reserved_words.end())
t.type = reserved_words.find(t.Codepoints())->second;
else
t.type = Wide::Lexer::TokenType::Identifier;
} else {
t.type = type;
}
begin = current;
tokens.push_back(arena.Allocate<Token>(t));
}
return tokens;
}
它由一个基于迭代器,回溯的有限状态机库提供支持,该库的长度约为400行。但是,很容易看出,我要做的就是构造简单的布尔运算,如and
,or
和not
,以及一些正则表达式样式的运算符(例如*
零或更多),eps
表示“匹配任何内容”并opt
表示“匹配”什么都不要消耗”。该库是完全通用的,并且基于迭代器。MakeEquality东西是一个简单的测试,用于检验是否相等*it
以及传入的值,而MakeRange是一个简单的<= >=
测试。
最终,我打算从回溯转向预测。
MakeEquality
片段吗?特别是该函数返回的对象。看起来很有趣。
首先,这里发生了不同的事情:
通常,我们希望词法分析器一次完成所有3个步骤,但是后者本质上比较困难,并且自动化存在一些问题(稍后将对此进行更多介绍)。
我知道的最惊人的词法分析器是Boost.Spirit.Qi。它使用表达式模板来生成您的词法分析器表达式,并且一旦习惯了其语法,代码就会感觉很整洁。尽管它的编译速度很慢(繁重的模板),所以最好隔离专用文件中的各个部分,以免在未使用它们时重新编译它们。
性能方面存在一些缺陷,Epoch编译器的作者在一篇文章中通过深入的剖析和调查Qi的工作原理来解释了他如何使速度提高1000倍。
最后,还有外部工具(Yacc,Bison等)生成的代码。
但是我答应写一篇关于自动进行语法验证的问题的文章。
例如,如果您签出Clang,您将认识到,与其使用生成的解析器和Boost.Spirit之类的东西,不如说他们开始使用通用的下降解析技术手动验证语法。当然这似乎是落后的吗?
实际上,有一个非常简单的原因:错误恢复。
C ++中的典型示例:
struct Immediate { } instanceOfImmediate;
struct Foo {}
void bar() {
}
注意错误?在声明之后,缺少分号Foo
。
这是一个常见的错误,Clang通过意识到它只是丢失而已,void
并且不是Foo
下一个声明的实例,而是下一个声明的一部分,可以整洁地进行恢复。这样可以避免难以诊断的隐式错误消息。
大多数自动化工具都没有(至少是显而易见的)方式来指定那些可能的错误以及如何从中恢复。通常,恢复需要进行一些语法分析,因此远非显而易见。
因此,在使用自动化工具时需要权衡取舍:虽然可以快速获取解析器,但它对用户的友好程度较低。
由于您想学习词法分析器的工作原理,因此我想您实际上想知道词法分析器生成器的工作原理。
词法分析器生成器采用词法规范(即规则列表(正则表达式-令牌对)),并生成词法分析器。然后,此生成的词法分析器可以根据此规则列表将输入(字符)字符串转换为令牌字符串。
最常用的方法主要包括通过非确定性自动机(NFA)将正则表达式转换为确定性有限自动机(DFA),以及一些细节。
可以在此处找到进行此转换的详细指南。请注意,我自己还没有阅读过,但是看起来不错。此外,几乎所有有关编译器构造的书都将在前几章中介绍这种转换。
如果您对有关该主题的课程的幻灯片感兴趣,那么毫无疑问,编译器构建课程中有无数的课程。在我的大学里,您可以在这里和这里找到这样的幻灯片。
在词法分析器中不常用或在文本中处理的东西很少,但仍然很有用:
首先,处理Unicode有点不平凡。问题在于ASCII输入只有8位宽,这意味着您可以轻松拥有DFA中每个状态的转换表,因为它们只有256个条目。但是,Unicode为16位宽(如果您使用UTF-16),则DFA中的每个条目都需要64k表。如果您有复杂的语法,这可能会开始占用相当多的空间。填充这些表也将花费大量时间。
或者,您可以生成间隔树。例如,范围树可能包含元组('a','z'),('A','Z'),这比具有完整表的存储效率要高得多。如果您保持不重叠的间隔,则可以为此目的使用任何平衡的二叉树。每个字符所需的位数是线性运行时间,因此在Unicode情况下为O(16)。但是,在最佳情况下,通常会少很多。
另一个问题是,通常生成的词法分析器实际上具有最坏情况的二次性能。尽管这种最坏情况的行为并不常见,但它可能会咬你。如果您遇到问题并想解决,可以找到描述如何实现线性时间的论文此处。
您可能希望能够像通常一样以字符串形式描述正则表达式。但是,将这些正则表达式描述解析为NFA(或首先可能是递归的中间结构)有点麻烦。为了解析正则表达式描述,Shunting Yard算法非常适合。维基百科似乎对该算法有广泛的介绍。