Menulis lexer di C ++

18

Apa sumber yang bagus tentang cara menulis lexer di C ++ (buku, tutorial, dokumen), apa sajakah teknik dan praktik yang baik?

Saya telah melihat di internet dan semua orang mengatakan untuk menggunakan generator lexer seperti lex. Saya tidak ingin melakukan itu, saya ingin menulis lexer dengan tangan.

sayap kanan
sumber
Ok, mengapa lex tidak baik untuk tujuan Anda?
CarneyCode
13
Saya ingin mempelajari cara kerja lexers. Saya tidak bisa melakukan itu dengan generator lexer.
dibuka kembali pada
11
Lex menghasilkan kode C menjijikkan. Siapa pun yang menginginkan lexer yang layak tidak menggunakan Lex.
DeadMG
5
@Giorgio: Kode yang dihasilkan adalah kode yang harus Anda antarmuka, dengan variabel global yang tidak aman yang menjijikkan, misalnya, dan itu adalah kode yang bug penghentian NULL yang Anda perkenalkan ke dalam aplikasi Anda.
DeadMG
1
@Iorgio: Pernahkah Anda men-debug output kode oleh Lex?
mattnz

Jawaban:

7

Perlu diingat bahwa setiap mesin status terbatas berhubungan dengan ekspresi reguler, yang sesuai dengan program terstruktur yang digunakan if dan whilepernyataan.

Jadi, misalnya, untuk mengenali bilangan bulat, Anda dapat memiliki mesin status:

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

atau ekspresi reguler:

digit digit*

atau kode terstruktur:

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

Secara pribadi, saya selalu menulis lexers menggunakan yang terakhir, karena IMHO tidak kurang jelas, dan tidak ada yang lebih cepat.

Mike Dunlavey
sumber
Saya berpikir bahwa jika ekspresi reguler menjadi sangat kompleks, demikian juga kode yang sesuai. Itu sebabnya generator lexer bagus: Saya biasanya hanya mengkodekan sebuah lexer sendiri jika bahasanya sangat sederhana.
Giorgio
1
@Iorgio: Mungkin ini masalah selera, tapi saya sudah membuat banyak parser dengan cara ini. Lexer tidak harus menangani apa pun di luar angka, tanda baca, kata kunci, pengidentifikasi, konstanta string, spasi putih, dan komentar.
Mike Dunlavey
Saya tidak pernah menulis parser yang kompleks dan semua lexer dan parser yang saya tulis juga kode tangan. Saya hanya ingin tahu bagaimana skala ini untuk bahasa reguler yang lebih kompleks: Saya belum pernah mencobanya tetapi saya membayangkan bahwa menggunakan generator (seperti lex) akan lebih ringkas. Saya akui saya tidak punya pengalaman dengan lex atau generator lain di luar beberapa contoh mainan.
Giorgio
1
Akan ada string yang Anda tambahkan *pc, kan? Seperti while(isdigit(*pc)) { value += pc; pc++; }. Kemudian setelah }Anda mengonversi nilainya menjadi angka dan menetapkannya sebagai token.
buka kembali
@ IPT: Untuk angka, saya hanya menghitungnya dengan cepat, mirip dengan n = n * 10 + (*pc++ - '0');. Ia mendapat sedikit lebih kompleks untuk floating point dan notasi 'e', ​​tetapi tidak buruk. Saya yakin saya bisa menyimpan sedikit kode dengan mengemas karakter ke buffer dan panggilan atofatau apa pun. Itu tidak akan berjalan lebih cepat.
Mike Dunlavey
9

Lexer adalah mesin negara yang terbatas. Oleh karena itu, mereka dapat dibangun oleh perpustakaan FSM tujuan umum. Namun, untuk keperluan pendidikan saya sendiri, saya menulis sendiri, menggunakan templat ekspresi. Inilah lexer saya:

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

Ini didukung oleh pustaka mesin negara berbasis iterator, pelacakan-kembali, hingga yang panjangnya ~ 400 baris. Namun, mudah untuk melihat bahwa yang harus saya lakukan adalah membangun operasi boolean sederhana, seperti and,, ordan not, dan beberapa operator gaya regex seperti *nol atau lebih, epsberarti "cocokkan apa saja" dan optberarti "cocokkan" apapun tetapi jangan mengkonsumsinya ". Perpustakaan sepenuhnya generik dan didasarkan pada iterator. Hal-hal MakeEquality adalah tes sederhana untuk kesetaraan antara *itdan nilai yang diteruskan, dan MakeRange adalah <= >=tes sederhana .

Akhirnya, saya berencana untuk beralih dari mundur ke prediktif.

DeadMG
sumber
2
Saya telah melihat beberapa lexers yang baru saja membaca token berikutnya ketika diminta oleh parser untuk melakukannya. Tampaknya Anda membaca seluruh file dan membuat daftar token. Apakah ada keuntungan khusus dari metode ini?
user673679
2
@DeadMG: Peduli membagikan MakeEqualitycuplikan? Secara khusus objek dikembalikan oleh fungsi itu. Terlihat sangat menarik.
Deathicon
3

Pertama-tama, ada beberapa hal berbeda yang terjadi di sini:

  • memecah daftar karakter kosong menjadi token
  • mengenali token tersebut (mengidentifikasi kata kunci, literal, tanda kurung, ...)
  • memverifikasi struktur tata bahasa umum

Secara umum, kami mengharapkan lexer untuk melakukan semua 3 langkah sekaligus, namun yang terakhir secara inheren lebih sulit dan ada beberapa masalah dengan otomatisasi (lebih lanjut tentang ini nanti).

Lexer paling menakjubkan yang saya tahu adalah Boost.Spirit.Qi . Ini menggunakan templat ekspresi untuk menghasilkan ekspresi lexer Anda, dan setelah terbiasa dengan sintaksnya, kode terasa sangat rapi. Ini mengkompilasi dengan sangat lambat (template berat), jadi yang terbaik adalah mengisolasi berbagai bagian dalam file khusus untuk menghindari kompilasi ulang ketika mereka belum disentuh.

Ada beberapa jebakan dalam kinerja, dan penulis kompilator Epoch menjelaskan bagaimana ia mendapat kecepatan 1000x dengan profiling intensif dan investigasi tentang bagaimana Qi bekerja dalam sebuah artikel .

Akhirnya, ada juga kode yang dihasilkan oleh alat eksternal (Yacc, Bison, ...).


Tetapi saya berjanji akan menulis tentang apa yang salah dengan mengotomatiskan verifikasi tata bahasa.

Jika Anda memeriksa Dentang, misalnya, Anda akan menyadari bahwa alih-alih menggunakan parser yang dihasilkan dan sesuatu seperti Boost.Spirit, sebagai gantinya mereka berangkat untuk memvalidasi tata bahasa secara manual menggunakan teknik Descent Parsing generik. Tentunya ini sepertinya terbelakang?

Bahkan, ada alasan yang sangat sederhana: pemulihan kesalahan .

Contoh khas, di C ++:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Perhatikan kesalahannya? Semi-colon hilang tepat setelah deklarasi Foo.

Ini adalah kesalahan umum, dan Clang pulih dengan rapi dengan menyadari bahwa itu hanya hilang dan voidbukan merupakan contoh Footetapi bagian dari deklarasi berikutnya. Ini menghindari kesulitan untuk mendiagnosis pesan kesalahan kriptik.

Sebagian besar alat otomatis tidak memiliki (setidaknya jelas) cara untuk menentukan kemungkinan kesalahan tersebut dan cara memulihkannya. Seringkali pemulihan memerlukan sedikit analisis sintaksis sehingga jauh dari bukti.


Jadi, ada trade-off yang terlibat dalam menggunakan alat otomatis: Anda mendapatkan parser Anda dengan cepat, tetapi itu kurang ramah pengguna.

Matthieu M.
sumber
3

Karena Anda ingin mempelajari cara kerja lexer, saya kira Anda sebenarnya ingin tahu cara kerja lexer generator.

Generator lexer mengambil spesifikasi leksikal, yang merupakan daftar aturan (pasangan regular-ekspresi-token), dan menghasilkan lexer. Lexer yang dihasilkan ini kemudian dapat mengubah string input (karakter) menjadi string token sesuai dengan daftar aturan ini.

Metode yang paling umum digunakan terutama terdiri dari mengubah ekspresi reguler menjadi automata terbatas deterministik (DFA) melalui automata nondeterministic automata (NFA), ditambah beberapa detail.

Panduan terperinci untuk melakukan transformasi ini dapat ditemukan di sini . Perhatikan bahwa saya belum membacanya sendiri, tetapi terlihat cukup bagus. Juga, hampir semua buku tentang konstruksi kompiler akan menampilkan transformasi ini dalam beberapa bab pertama.

Jika Anda tertarik dengan slide kuliah tentang topik ini, tidak ada keraguan jumlah mereka yang tak ada habisnya dari kursus konstruksi kompiler. Dari universitas saya, Anda dapat menemukan slide seperti itu di sini dan di sini .

Ada beberapa hal lagi yang biasanya tidak digunakan dalam lexers atau diperlakukan dalam teks, tetapi tetap sangat berguna:

Pertama, menangani Unicode agak tidak trivial. Masalahnya adalah input ASCII hanya selebar 8 bit, yang berarti Anda dapat dengan mudah memiliki tabel transisi untuk setiap keadaan di DFA, karena mereka hanya memiliki 256 entri. Namun, Unicode, dengan lebar 16 bit (jika Anda menggunakan UTF-16), membutuhkan tabel 64k untuk setiap entri di DFA. Jika Anda memiliki tata bahasa yang rumit, ini mungkin mulai memakan banyak ruang. Mengisi tabel ini juga mulai memakan sedikit waktu.

Atau, Anda dapat membuat pohon interval. Pohon rentang dapat berisi tupel ('a', 'z'), ('A', 'Z') misalnya, yang jauh lebih efisien memori daripada memiliki tabel penuh. Jika Anda mempertahankan interval yang tidak tumpang tindih, Anda dapat menggunakan pohon biner seimbang untuk tujuan ini. Waktu berjalan linear dalam jumlah bit yang Anda butuhkan untuk setiap karakter, jadi O (16) dalam kasus Unicode. Namun, dalam kasus terbaik, biasanya akan sedikit lebih sedikit.

Satu masalah lagi adalah bahwa lexers seperti yang biasa dihasilkan sebenarnya memiliki kinerja kuadratik terburuk. Meskipun perilaku terburuk ini tidak umum terlihat, ini mungkin menggigit Anda. Jika Anda mengalami masalah dan ingin menyelesaikannya, makalah yang menjelaskan cara mencapai waktu linier dapat ditemukan di sini .

Anda mungkin ingin dapat menggambarkan ekspresi reguler dalam bentuk string, seperti yang biasanya muncul. Namun, menguraikan deskripsi ekspresi reguler ini ke dalam NFA (atau mungkin struktur menengah rekursif pertama) adalah sedikit masalah ayam-telur. Untuk menguraikan deskripsi ekspresi reguler, algoritma Shunting Yard sangat cocok. Wikipedia tampaknya memiliki halaman yang luas tentang algoritme .

Alex ten Brink
sumber