用C ++编写词法分析器


18

有哪些关于如何使用C ++编写词法分析器的好资源(书籍,教程,文档),有哪些好的技术和实践?

我在互联网上看过,每个人都说要使用像lex这样的词法生成器。我不想这样做,我想手动编写一个词法分析器。


好的,为什么lex不适合您的目的?
CarneyCode

13
我想学习词法分析器的工作原理。我无法使用词法生成器来做到这一点。
2012年

11
Lex生成令人作呕的C代码。任何想要像样的词法分析器的人都不会使用Lex。
DeadMG 2012年

5
@Giorgio:生成的代码是您必须与之交互的代码,例如,带有令人讨厌的非线程安全的全局变量,并且这是您要将NULL终止错误引入到应用程序中的代码。
DeadMG 2012年

1
@Giorgio:您是否曾经调试过Lex输出的代码?
mattnz'1

Answers:


7

请记住,每个有限状态机都对应一个正则表达式,该正则表达式对应于使用ifwhile语句的结构化程序。

因此,例如,要识别整数,您可以使用状态机:

0: digit -> 1
1: digit -> 1

或正则表达式:

digit digit*

或结构化代码:

if (isdigit(*pc)){
  while(isdigit(*pc)){
    pc++;
  }
}

就个人而言,我总是使用后者来编写词法分析器,因为恕我直言,它并不清楚,也没有更快的方法。


我认为,如果正则表达式变得非常复杂,则相应的代码也是如此。这就是lexer generator很好的原因:如果语言非常简单,我通常只会自己编写lexer代码。
乔治

1
@Giorgio:也许这是一个品味问题,但是我以此方式构建了许多解析器。除数字,标点,关键字,标识符,字符串常量,空格和注释外,词法分析器无需处理任何其他内容。
Mike Dunlavey,2012年

我从未编写过复杂的解析器,而且我编写的所有词法分析器和解析器也都经过手工编码。我只是想知道它如何适应更复杂的常规语言:我从未尝试过,但是我想使用生成器(如lex)会更紧凑。我承认除了一些玩具示例外,我没有使用lex或其他生成器的经验。
Giorgio

1
您会附加一个字符串*pc,对吗?喜欢while(isdigit(*pc)) { value += pc; pc++; }。然后,}您将值转换为数字并将其分配给令牌。
2012年

@WTP:对于数字,我只是即时计算它们,类似于n = n * 10 + (*pc++ - '0');。对于浮点数和“ e”表示法,它有点复杂,但还不错。我确定可以通过将字符打包到缓冲区中并调用atof或其他方式来保存一些代码。它不会运行得更快。
Mike Dunlavey,2012年

9

词法分析器是有限状态机。因此,它们可以由任何通用的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行。但是,很容易看出,我要做的就是构造简单的布尔运算,如andornot,以及一些正则表达式样式的运算符(例如*零或更多),eps表示“匹配任何内容”并opt表示“匹配”什么都不要消耗”。该库是完全通用的,并且基于迭代器。MakeEquality东西是一个简单的测试,用于检验是否相等*it以及传入的值,而MakeRange是一个简单的<= >=测试。

最终,我打算从回溯转向预测。


2
我已经看到几个词法分析器,它们在解析器要求时才读取下一个标记。您的似乎遍历了整个文件并列出了令牌的列表。这种方法有什么特别的优点吗?
user673679 2013年

2
@DeadMG:想要分享MakeEquality片段吗?特别是该函数返回的对象。看起来很有趣。
Deathicon

3

首先,这里发生了不同的事情:

  • 将裸字符列表拆分为标记
  • 识别这些标记(识别关键字,文字,方括号等)
  • 验证一般语法结构

通常,我们希望词法分析器一次完成所有3个步骤,但是后者本质上比较困难,并且自动化存在一些问题(稍后将对此进行更多介绍)。

我知道的最惊人的词法分析器是Boost.Spirit.Qi。它使用表达式模板来生成您的词法分析器表达式,并且一旦习惯了其语法,代码就会感觉很整洁。尽管它的编译速度很慢(繁重的模板),所以最好隔离专用文件中的各个部分,以免在未使用它们时重新编译它们。

性能方面存在一些缺陷,Epoch编译器的作者在一篇文章中通过深入的剖析和调查Qi的工作原理解释了他如何使速度提高1000倍。

最后,还有外部工具(Yacc,Bison等)生成的代码。


但是我答应写一篇关于自动进行语法验证的问题的文章。

例如,如果您签出Clang,您将认识到,与其使用生成的解析器和Boost.Spirit之类​​的东西,不如说他们开始使用通用的下降解析技术手动验证语法。当然这似乎是落后的吗?

实际上,有一个非常简单的原因:错误恢复

C ++中的典型示例:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

注意错误?在声明之后,缺少分号Foo

这是一个常见的错误,Clang通过意识到它只是丢失而已,void并且不是Foo下一个声明的实例,而是下一个声明的一部分,可以整洁地进行恢复。这样可以避免难以诊断的隐式错误消息。

大多数自动化工具都没有(至少是显而易见的)方式来指定那些可能的错误以及如何从中恢复。通常,恢复需要进行一些语法分析,因此远非显而易见。


因此,在使用自动化工具时需要权衡取舍:虽然可以快速获取解析器,但它对用户的友好程度较低。


3

由于您想学习词法分析器的工作原理,因此我想您实际上想知道词法分析器生成器的工作原理。

词法分析器生成器采用词法规范(即规则列表(正则表达式-令牌对)),并生成词法分析器。然后,此生成的词法分析器可以根据此规则列表将输入(字符)字符串转换为令牌字符串。

最常用的方法主要包括通过非确定性自动机(NFA)将正则表达式转换为确定性有限自动机(DFA),以及一些细节。

可以在此处找到进行此转换的详细指南。请注意,我自己还没有阅读过,但是看起来不错。此外,几乎所有有关编译器构造的书都将在前几章中介绍这种转换。

如果您对有关该主题的课程的幻灯片感兴趣,那么毫无疑问,编译器构建课程中有无数的课程。在我的大学里,您可以在这里这里找到这样的幻灯片。

在词法分析器中不常用或在文本中处理的东西很少,但仍然很有用:

首先,处理Unicode有点不平凡。问题在于ASCII输入只有8位宽,这意味着您可以轻松拥有DFA中每个状态的转换表,因为它们只有256个条目。但是,Unicode为16位宽(如果您使用UTF-16),则DFA中的每个条目都需要64k表。如果您有复杂的语法,这可能会开始占用相当多的空间。填充这些表也将花费大量时间。

或者,您可以生成间隔树。例如,范围树可能包含元组('a','z'),('A','Z'),这比具有完整表的存储效率要高得多。如果您保持不重叠的间隔,则可以为此目的使用任何平衡的二叉树。每个字符所需的位数是线性运行时间,因此在Unicode情况下为O(16)。但是,在最佳情况下,通常会少很多。

另一个问题是,通常生成的词法分析器实际上具有最坏情况的二次性能。尽管这种最坏情况的行为并不常见,但它可能会咬你。如果您遇到问题并想解决,可以找到描述如何实现线性时间的论文此处

您可能希望能够像通常一样以字符串形式描述正则表达式。但是,将这些正则表达式描述解析为NFA(或首先可能是递归的中间结构)有点麻烦。为了解析正则表达式描述,Shunting Yard算法非常适合。维基百科似乎对该算法有广泛的介绍。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.