C ++ ile lexer yazma


18

C ++ 'da bir lexer yazma konusunda iyi kaynaklar nelerdir (kitaplar, öğreticiler, belgeler), bazı iyi teknikler ve uygulamalar nelerdir?

İnternete baktım ve herkes lex gibi bir lexer jeneratörü kullanmayı söylüyor. Bunu yapmak istemiyorum, elle bir lexer yazmak istiyorum.


Tamam, lex neden amacınız için iyi değil?
CarneyCode

13
Sözlükçülerin nasıl çalıştığını öğrenmek istiyorum. Bunu bir lexer jeneratörü ile yapamam.
sağda

11
Lex iğrenç C kodu üretir. İyi bir sözlük isteyen herkes Lex'i kullanmaz.
DeadMG

5
@Giorgio: Oluşturulan kod, örneğin iş parçacığı için güvenli olmayan genel değişkenlerle arayüz oluşturmanız gereken koddur ve NULL sonlandırma hatalarını uygulamanıza tanıttığınız koddur.
DeadMG

1
@Giorgio: Hiç Lex tarafından kod çıktısında hata ayıklamak zorunda kaldınız mı?
mattnz

Yanıtlar:


7

Her sonlu durum makinesinin ifve whileifadelerini kullanarak yapılandırılmış bir programa karşılık gelen normal bir ifadeye karşılık geldiğini unutmayın .

Örneğin, tamsayıları tanımak için durum makinesine sahip olabilirsiniz:

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

veya normal ifade:

digit digit*

veya yapılandırılmış kod:

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

Şahsen, her zaman ikincisini kullanarak sözlük yazarım, çünkü IMHO daha az net değil ve daha hızlı bir şey yok.


Düzenli ifade çok karmaşık hale gelirse, ilgili kod da öyle düşünüyorum. Bu yüzden lexer üreteci iyidir: Bir lexer'ı sadece dil çok basitse kendim kodlayabilirim.
Giorgio

1
@Giorgio: Belki de bir zevk meselesi, ama bu şekilde birçok ayrıştırıcı yaptım. Lexer, sayılar, noktalama işaretleri, anahtar kelimeler, tanımlayıcılar, dize sabitleri, boşluk ve yorumların ötesinde bir şey işlemek zorunda değildir.
Mike Dunlavey

Hiç karmaşık bir ayrıştırıcı yazmadım ve yazdığım tüm sözcük ve ayrıştırıcılar da elle kodlandı. Bunun daha karmaşık düzenli diller için nasıl ölçeklendiğini merak ediyorum: Hiç denemedim ama bir jeneratör (lex gibi) kullanmanın daha kompakt olacağını hayal ediyorum. Bazı oyuncak örneklerinin ötesinde lex veya diğer jeneratörlerle deneyimim olmadığını itiraf ediyorum.
Giorgio

1
Eklediğiniz bir dize olurdu *pc, değil mi? Gibi while(isdigit(*pc)) { value += pc; pc++; }. Daha sonra }değeri bir sayıya dönüştürüp bir jetona atarsınız.
sağda

@WTP: Sayılar için, onları anında benzer şekilde hesaplıyorum n = n * 10 + (*pc++ - '0');. Kayan nokta ve 'e' gösterimi için biraz daha karmaşık hale gelir, ancak kötü değil. Karakterleri bir arabellek içine paketleyip arayarak atofya da her neyse küçük bir kod kaydedebileceğime eminim . Daha hızlı koşmazdı.
Mike Dunlavey

9

Lexers sonlu durum makineleridir. Bu nedenle, herhangi bir genel amaçlı FSM kütüphanesi tarafından inşa edilebilirler. Ancak kendi eğitimim amacıyla ifade şablonlarını kullanarak kendi yazımı yazdım. İşte benim lexer:

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 satır uzunluğunda yineleyici tabanlı, geri izleme, sonlu durum makine kütüphanesi tarafından desteklenmektedir. Ancak, tüm ı yapı basit boolean operasyonları gibiydi yapmak zorunda olduğunu görmek kolaydır and, orve not, ve regex tarzı operatörleri bir çift gibi *sıfır veya-daha, için eps"maç şey" demek ve optdemek "maçın "ama onu tüketme". Kütüphane tamamen geneldir ve yineleyicilere dayanmaktadır. MakeEquality şeyler arasında eşitlik *itve aktarılan değer için basit bir <= >=test ve MakeRange basit bir testtir.

Sonunda, geri izlemeden öngörüye geçmeyi planlıyorum.


2
Ayrıştırıcı tarafından istendiğinde bir sonraki jetonu okuyan birkaç sözlük gördüm. Sizinki bütün bir dosyadan geçiyor ve jetonlar listesi yapıyor gibi görünüyor. Bu yöntemin belirli bir avantajı var mı?
user673679

2
@DeadMG: MakeEqualitySnippet'i paylaşmak ister misiniz? Özellikle bu işlev tarafından döndürülen nesne. Çok ilginç görünüyor.
Deathicon

3

Her şeyden önce, burada farklı şeyler oluyor:

  • çıplak karakterler listesini jetonlara bölme
  • bu belirteçleri tanıma (anahtar kelimeleri, değişmez değerleri, parantezleri, ... tanımlama)
  • genel dilbilgisi yapısını doğrulama

Genel olarak, bir lexer'ın 3 adımın hepsini bir seferde yapmasını bekliyoruz, ancak ikincisi doğal olarak daha zordur ve otomasyonla ilgili bazı sorunlar vardır (daha sonra bu konuda daha fazla).

Bildiğim en şaşırtıcı lexer Boost.Spirit.Qi . Lexer ifadelerinizi oluşturmak için ifade şablonlarını kullanır ve sözdizimine alıştıktan sonra kod gerçekten temiz hisseder. Yine de çok yavaş derler (ağır şablonlar), bu yüzden dokunulmadığında yeniden derlenmelerini önlemek için özel dosyalardaki çeşitli bölümleri izole etmek en iyisidir.

Performansta bazı tuzaklar var ve Epoch derleyicisinin yazarı, Qi'nin bir makalede nasıl çalıştığına dair yoğun profil oluşturma ve araştırma yaparak 1000 kat hızlandırmayı nasıl açıkladığını açıklıyor .

Son olarak, harici araçlar (Yacc, Bison, ...) tarafından üretilen kod da vardır.


Ama dilbilgisi doğrulamasının otomatikleştirilmesinde neyin yanlış olduğuna dair bir yazı sözü verdim.

Örneğin Clang'ı kontrol ederseniz, oluşturulan bir ayrıştırıcı ve Boost.Spirit gibi bir şey kullanmak yerine, genel bir İniş Ayrıştırma tekniği kullanarak dilbilgisini manuel olarak doğrulamaya hazırlandıklarını fark edeceksiniz. Elbette bu geri görünüyor?

Aslında, çok basit bir neden var: hata giderme .

C ++ 'da tipik örnek:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Hatayı fark ettiniz mi? İlanından hemen sonra eksik bir noktalı virgül Foo.

Bu yaygın bir hatadır ve Clang, basitçe eksik olduğunu ve voidbir Foosonraki bildirimin bir örneği değil, bir parçası olduğunu fark ederek düzgün bir şekilde iyileşir . Bu, şifreli hata mesajlarını teşhis etmekte zorlanır.

Çoğu otomatik aracın bu olası hataları ve bunlardan nasıl kurtulacağını belirlemenin hiçbir yolu yoktur (en azından açık). Genellikle iyileşme biraz sözdizimsel analiz gerektirir, bu yüzden açıktır.


Bu nedenle, otomatik bir araç kullanmayla ilgili değiş tokuş vardır: ayrıştırıcıyı hızlı bir şekilde alırsınız, ancak daha az kullanıcı dostudur.


3

Lexer'ların nasıl çalıştığını öğrenmek istediğiniz için, sanırım lexer jeneratörlerinin nasıl çalıştığını bilmek istediğinizi varsayalım.

Bir lexer üreteci, kuralların bir listesi olan (normal ifade-token çiftleri) sözcüksel bir şartname alır ve bir lexer oluşturur. Bu sonuçtaki lexer daha sonra bu kural listesine göre bir girdi (karakter) dizgisini bir belirteç dizgisine dönüştürebilir.

En yaygın olarak kullanılan yöntem, normal bir ifadeyi belirsiz olmayan bir otomata (NFA) ve birkaç ayrıntıyı kullanarak deterministik bir sonlu otomata (DFA) dönüştürmekten oluşur.

Bu dönüşümü yapmak için ayrıntılı bir rehber burada bulunabilir . Kendim okumadığımı unutmayın, ama oldukça iyi görünüyor. Ayrıca, derleyici yapımıyla ilgili hemen hemen her kitap, ilk birkaç bölümde bu dönüşümü sunacaktır.

Eğer konuyla ilgili derslerin ders slaytları ile ilgileniyorsanız, derleyici sayısının derleyici inşaatı derslerinden şüphesiz var. Üniversitemden bu tür slaytları burada ve burada bulabilirsiniz .

Sözlüklerde yaygın olarak kullanılmayan veya metinlerde tedavi edilen, ancak yine de oldukça yararlı olan birkaç şey daha vardır:

İlk olarak, Unicode kullanımı biraz önemsizdir. Sorun, ASCII girişinin sadece 8 bit genişliğidir, bu da DFA'daki her durum için kolayca bir geçiş tablosuna sahip olabileceğiniz anlamına gelir, çünkü bunlar sadece 256 girişe sahiptir. Ancak, Unicode, 16 bit genişliğinde (UTF-16 kullanıyorsanız), DFA'daki her giriş için 64k tablo gerektirir. Karmaşık gramerleriniz varsa, bu biraz yer kaplayabilir. Bu tabloları doldurmak da biraz zaman almaya başlar.

Alternatif olarak, aralıklı ağaçlar oluşturabilirsiniz. Bir aralık ağacında, tuples ('a', 'z'), ('A', 'Z') bulunabilir; Çakışmayan aralıkları koruyorsanız, bu amaçla herhangi bir dengeli ikili ağaç kullanabilirsiniz. Çalışma süresi, her karakter için ihtiyacınız olan bit sayısında doğrusaldır, bu nedenle Unicode durumunda O (16). Ancak, en iyi durumda, genellikle biraz daha az olacaktır.

Bir başka konu, yaygın olarak üretilen lexerların aslında en kötü durumda ikinci dereceden bir performansa sahip olmasıdır. Bu en kötü durum davranışı sık görülmese de, sizi ısırabilir. Problemle karşılaşırsanız ve çözmek istiyorsanız, doğrusal zamanın nasıl elde edileceğini açıklayan bir makale burada bulunabilir .

Muhtemelen normal ifadeleri normal olarak göründükleri gibi dize biçiminde tanımlamak isteyeceksinizdir. Bununla birlikte, bu düzenli ekspresyon açıklamalarını NFA'lara (veya muhtemelen tekrarlayan bir ara yapıya) ayrıştırmak, bir tavuk yumurtası problemidir. Düzenli ifade açıklamalarını ayrıştırmak için, Shunting Yard algoritması çok uygundur. Wikipedia'nın algoritma hakkında kapsamlı bir sayfası var gibi görünüyor .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.