Viết một từ vựng trong C ++


18

Tài nguyên tốt về cách viết một từ vựng trong C ++ (sách, hướng dẫn, tài liệu), một số kỹ thuật và thực hành tốt là gì?

Tôi đã xem trên internet và mọi người nói hãy sử dụng một trình tạo từ vựng như lexer. Tôi không muốn làm điều đó, tôi muốn viết một từ vựng bằng tay.


Ok, tại sao lex không tốt cho mục đích của bạn?
CarneyCode

13
Tôi muốn tìm hiểu làm thế nào lexers hoạt động. Tôi không thể làm điều đó với một trình tạo lexer.
đúng

11
Lex tạo mã C kinh tởm. Bất cứ ai muốn một lexer đàng hoàng đều không sử dụng Lex.
DeadMG

5
@Giorgio: Mã được tạo là mã mà bạn phải giao tiếp, với các biến toàn cục không an toàn chủ đề, ví dụ, và đó là mã có lỗi chấm dứt NULL mà bạn giới thiệu vào ứng dụng của mình.
DeadMG

1
@Giorgio: Bạn đã bao giờ phải gỡ lỗi đầu ra mã bằng Lex chưa?
mattnz

Câu trả lời:


7

Hãy nhớ rằng mọi máy trạng thái hữu hạn tương ứng với một biểu thức chính quy, tương ứng với một chương trình có cấu trúc sử dụng ifvà các whilecâu lệnh.

Vì vậy, ví dụ, để nhận ra số nguyên bạn có thể có máy trạng thái:

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

hoặc biểu thức chính quy:

digit digit*

hoặc mã có cấu trúc:

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

Cá nhân, tôi luôn viết lexers bằng cách sử dụng cái sau, bởi vì IMHO nó không kém phần rõ ràng, và không có gì nhanh hơn.


Tôi nghĩ rằng nếu biểu thức chính quy trở nên rất phức tạp, thì mã tương ứng cũng vậy. Đó là lý do tại sao trình tạo lexer tốt: thông thường tôi sẽ chỉ tự viết mã cho một từ vựng nếu ngôn ngữ rất đơn giản.
Giorgio

1
@Giorgio: Có thể đó là vấn đề của hương vị, nhưng tôi đã xây dựng nhiều trình phân tích cú pháp theo cách này. Nhà từ vựng không phải xử lý bất cứ điều gì ngoài số, dấu chấm câu, từ khóa, định danh, hằng chuỗi, khoảng trắng và nhận xét.
Mike Dunlavey

Tôi chưa bao giờ viết một trình phân tích cú pháp phức tạp và tất cả các từ vựng và trình phân tích cú pháp tôi đã viết cũng được mã hóa bằng tay. Tôi chỉ tự hỏi làm thế nào thang đo này cho các ngôn ngữ thông thường phức tạp hơn: Tôi chưa bao giờ thử nó nhưng tôi tưởng tượng rằng việc sử dụng một trình tạo (như lex) sẽ nhỏ gọn hơn. Tôi thừa nhận tôi không có kinh nghiệm với lex hoặc các máy phát điện khác ngoài một số ví dụ về đồ chơi.
Giorgio

1
Sẽ có một chuỗi bạn nối *pcvào, phải không? Thích while(isdigit(*pc)) { value += pc; pc++; }. Sau đó, sau khi }bạn chuyển đổi giá trị thành một số và gán nó vào mã thông báo.
đúng

@WTP: Đối với các số, tôi chỉ cần tính toán chúng một cách nhanh chóng, tương tự như n = n * 10 + (*pc++ - '0');. Nó phức tạp hơn một chút đối với dấu phẩy động và ký hiệu 'e', ​​nhưng không tệ. Tôi chắc rằng tôi có thể lưu một ít mã bằng cách đóng gói các ký tự vào bộ đệm và gọi atofhoặc bất cứ thứ gì. Nó sẽ không chạy nhanh hơn.
Mike Dunlavey

9

Lexers là máy trạng thái hữu hạn. Do đó, chúng có thể được xây dựng bởi bất kỳ thư viện FSM đa năng nào. Tuy nhiên, với mục đích giáo dục của riêng tôi, tôi đã tự viết, sử dụng các mẫu biểu thức. Đây là từ điển của tôi:

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;
}

Nó được hỗ trợ bởi một thư viện máy trạng thái hữu hạn, theo dõi ngược, dựa trên vòng lặp có độ dài ~ 400 dòng. Tuy nhiên, thật dễ dàng để thấy rằng tất cả những gì tôi phải làm là xây dựng các hoạt động boolean đơn giản, như and, ornot, và một vài toán tử kiểu regex như *từ 0 trở lên, epscó nghĩa là "khớp bất cứ thứ gì" và optcó nghĩa là "khớp bất cứ điều gì nhưng không tiêu thụ nó ". Thư viện hoàn toàn chung chung và dựa trên các trình vòng lặp. Công cụ MakeEquality là một thử nghiệm đơn giản cho sự bằng nhau giữa *itvà giá trị được truyền vào và MakeRange là một <= >=thử nghiệm đơn giản .

Cuối cùng, tôi dự định chuyển từ quay lui sang dự đoán.


2
Tôi đã thấy một số từ vựng chỉ đọc mã thông báo tiếp theo khi được trình phân tích cú pháp yêu cầu để làm như vậy. Bạn dường như đi qua toàn bộ tệp và tạo danh sách mã thông báo. Có bất kỳ lợi thế đặc biệt phương pháp này?
dùng673679

2
@DeadMG: Muốn chia sẻ MakeEqualityđoạn trích? Cụ thể đối tượng được trả về bởi hàm đó. Trông rất thú vị.
Deathicon

3

Trước hết, có những điều khác nhau đang diễn ra ở đây:

  • chia danh sách nhân vật trần thành mã thông báo
  • nhận dạng các mã thông báo đó (xác định từ khóa, nghĩa đen, dấu ngoặc, ...)
  • kiểm tra cấu trúc ngữ pháp chung

Nói chung, chúng tôi hy vọng một lexer sẽ thực hiện cả 3 bước trong một lần, tuy nhiên bước sau khó khăn hơn và có một số vấn đề với tự động hóa (sẽ nói thêm về điều này sau).

Từ vựng tuyệt vời nhất mà tôi biết là Boost.Sprite.Qi . Nó sử dụng các mẫu biểu thức để tạo các biểu thức lexer của bạn và khi đã quen với cú pháp của nó, mã cảm thấy thực sự gọn gàng. Mặc dù vậy, nó biên dịch rất chậm (các mẫu nặng), vì vậy tốt nhất nên cách ly các phần khác nhau trong các tệp chuyên dụng để tránh biên dịch lại chúng khi chúng chưa được chạm vào.

Có một số cạm bẫy trong hiệu suất, và tác giả của trình biên dịch Epoch giải thích cách anh ta tăng tốc 1000 lần bằng cách lập hồ sơ và điều tra chuyên sâu về cách thức Qi hoạt động trong một bài viết .

Cuối cùng, cũng có mã được tạo bởi các công cụ bên ngoài (Yacc, Bison, ...).


Nhưng tôi đã hứa viết một bài về những gì sai khi tự động xác minh ngữ pháp.

Ví dụ, nếu bạn kiểm tra Clang, bạn sẽ nhận ra rằng thay vì sử dụng trình phân tích cú pháp được tạo và một cái gì đó như Boost.Sprite, thay vào đó họ bắt đầu xác thực ngữ pháp bằng cách sử dụng kỹ thuật Phân tích cú pháp chung chung. Chắc chắn điều này có vẻ lạc hậu?

Trong thực tế, có một lý do rất đơn giản: phục hồi lỗi .

Ví dụ điển hình, trong C ++:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Nhận thấy lỗi? Một dấu chấm phẩy mất tích ngay sau khi tuyên bố Foo.

Đó là một lỗi phổ biến và Clang phục hồi gọn gàng bằng cách nhận ra rằng nó chỉ đơn giản là thiếu và voidkhông phải là một trường hợp củaFoo mà là một phần của tuyên bố tiếp theo. Điều này tránh khó chẩn đoán các thông báo lỗi khó hiểu.

Hầu hết các công cụ tự động không có cách (ít nhất là rõ ràng) về việc chỉ định những lỗi có thể xảy ra và cách phục hồi từ chúng. Thường phục hồi đòi hỏi một chút phân tích cú pháp để nó không rõ ràng.


Vì vậy, có sự đánh đổi liên quan đến việc sử dụng một công cụ tự động: bạn nhanh chóng nhận được trình phân tích cú pháp, nhưng nó ít thân thiện với người dùng hơn.


3

Vì bạn muốn tìm hiểu cách thức hoạt động của lexer, tôi cho rằng bạn thực sự muốn biết làm thế nào các trình tạo lexer hoạt động.

Một trình tạo từ vựng có một đặc tả từ vựng, đó là một danh sách các quy tắc (các cặp mã thông báo biểu thức chính quy) và tạo ra một từ vựng. Sau đó, lexer kết quả này có thể chuyển đổi một chuỗi đầu vào (ký tự) thành một chuỗi mã thông báo theo danh sách quy tắc này.

Phương pháp được sử dụng phổ biến nhất chủ yếu bao gồm chuyển đổi một biểu thức chính quy thành một automata hữu hạn xác định (DFA) thông qua một automata không điều kiện (NFA), cùng với một vài chi tiết.

Một hướng dẫn chi tiết về việc thực hiện chuyển đổi này có thể được tìm thấy ở đây . Lưu ý rằng tôi đã không đọc nó cho mình, nhưng nó trông khá tốt. Ngoài ra, bất kỳ cuốn sách nào về xây dựng trình biên dịch sẽ có sự chuyển đổi này trong một vài chương đầu tiên.

Nếu bạn quan tâm đến các bài giảng của các khóa học về chủ đề này, không có nghi ngờ gì về số lượng vô tận của chúng từ các khóa học về xây dựng trình biên dịch. Từ trường đại học của tôi, bạn có thể tìm thấy các slide như vậy ở đâyđây .

Có một vài điều nữa thường không được sử dụng trong các từ vựng hoặc được xử lý trong các văn bản, nhưng dù sao cũng khá hữu ích:

Thứ nhất, việc xử lý Unicode có phần không cần thiết. Vấn đề là đầu vào ASCII chỉ rộng 8 bit, điều đó có nghĩa là bạn có thể dễ dàng có bảng chuyển đổi cho mọi trạng thái trong DFA, vì chúng chỉ có 256 mục. Tuy nhiên, Unicode, rộng 16 bit (nếu bạn sử dụng UTF-16), yêu cầu 64k bảng cho mỗi mục trong DFA. Nếu bạn có ngữ pháp phức tạp, điều này có thể bắt đầu chiếm khá nhiều không gian. Việc lấp đầy các bảng này cũng bắt đầu mất khá nhiều thời gian.

Ngoài ra, bạn có thể tạo cây khoảng. Ví dụ, một cây phạm vi có thể chứa các bộ dữ liệu ('a', 'z'), ('A', 'Z'), giúp tiết kiệm bộ nhớ hơn rất nhiều so với việc có đầy đủ bảng. Nếu bạn duy trì các khoảng không chồng chéo, bạn có thể sử dụng bất kỳ cây nhị phân cân bằng nào cho mục đích này. Thời gian chạy là tuyến tính theo số bit bạn cần cho mỗi ký tự, vì vậy O (16) trong trường hợp Unicode. Tuy nhiên, trong trường hợp tốt nhất, nó thường sẽ ít hơn một chút.

Một vấn đề nữa là các từ vựng như thường được tạo ra thực sự có hiệu suất bậc hai trong trường hợp xấu nhất. Mặc dù hành vi trong trường hợp xấu nhất này không thường thấy, nhưng nó có thể cắn bạn. Nếu bạn gặp phải vấn đề và muốn giải quyết nó, một bài viết mô tả cách đạt được thời gian tuyến tính có thể được tìm thấy ở đây .

Bạn có thể muốn có thể mô tả các biểu thức thông thường ở dạng chuỗi, như chúng thường xuất hiện. Tuy nhiên, phân tích các mô tả biểu thức chính quy này thành NFA (hoặc có thể là cấu trúc trung gian đệ quy trước) là một vấn đề của trứng gà. Để phân tích các mô tả biểu thức chính quy, thuật toán Shunting Yard rất phù hợp. Wikipedia dường như có một trang mở rộng về thuật toán .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.