Написання лексеми на C ++


18

Які хороші ресурси щодо того, як написати лексему на C ++ (книги, навчальні посібники, документи), які хороші методи та практики?

Я подивився в Інтернеті, і всі кажуть використовувати генератор лексерів, як Lex. Я не хочу цього робити, я хочу написати лексеру вручну.


Гаразд, чому лекс не корисний для вашої мети?
CarneyCode

13
Хочу дізнатися, як працюють лексеми. Я не можу зробити це з генератором лексерів.
праворуч

11
Lex створює огидний код C. Той, хто хоче гідного лексеру, не використовує Lex.
DeadMG

5
@Giorgio: Сгенерований код - це код, з яким ви маєте взаємодіяти, наприклад, із огидними глобальними змінними, що не стосуються потоків, і це код, чиї помилки із завершенням NULL ви вводите у свою програму.
DeadMG

1
@Giorgio: Вам коли-небудь доводилося налагоджувати вихідний код Lex?
mattnz

Відповіді:


7

Майте на увазі, що кожній машині кінцевих станів відповідає регулярний вираз, який відповідає структурованій програмі з використанням ifіwhile операторами.

Так, наприклад, для розпізнавання цілих чисел у вас може бути державна машина:

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

або регулярний вираз:

digit digit*

або структурований код:

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

Особисто я завжди пишу лексеми, використовуючи останні, тому що IMHO це не менш зрозуміло, і немає нічого швидшого.


Я думаю, що якщо регулярний вираз стає дуже складним, то і відповідний код. Ось чому генератор лексерів хороший: я б зазвичай кодував лексери сам, якщо мова дуже проста.
Джорджіо

1
@Giorgio: Можливо, це питання смаку, але я створив багато аналізаторів таким чином. Лексеру не потрібно обробляти нічого, крім цифр, пунктуації, ключових слів, ідентифікаторів, рядкових констант, пробілів та коментарів.
Майк Данлаве

Я ніколи не писав складного аналізатора, і всі лексеми та аналізатори, які я написав, також були кодовані вручну. Мені просто цікаво, як ця шкала для більш складних регулярних мов: я ніколи її не пробував, але думаю, що використання генератора (як Lex) було б більш компактним. Зізнаюся, я не маю досвіду роботи з lex та іншими генераторами за винятком деяких іграшкових прикладів.
Джорджіо

1
Буде рядок, який ви додаєте *pc, правда? Як while(isdigit(*pc)) { value += pc; pc++; }. Потім після }ви перетворите значення в число і призначите це маркер.
праворуч

@WTP: Для чисел я просто обчислюю їх на ходу, подібно до n = n * 10 + (*pc++ - '0');. Він стає трохи складнішим для плаваючої точки та позначення 'e', ​​але непогано. Я впевнений, що міг би зберегти трохи коду, запакувавши символи в буфер і подзвонивши atofчи що завгодно. Він би не запустився швидше.
Майк Данлаве

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 рядків. Однак легко зрозуміти, що все, що мені довелося зробити, - це сконструювати прості булеві операції, такі як and, orі not, і пара операторів стилю regex, як *для нуля або більше, epsщоб означати "відповідати будь-чому" і optозначати "відповідати нічого, але не споживайте ". Бібліотека повністю родова та заснована на ітераторах. Матеріал MakeEquality - це простий тест на рівність *itі передане значення, а MakeRange - простий <= >=тест.

Врешті-решт я планую перейти від зворотного треку до прогнозуючого.


2
Я бачив кілька лекерів, які просто читали наступний маркер, коли про це просив аналізатор. Здається, ваш файл пройшов цілий файл і склав список жетонів. Чи є якась перевага цього методу?
користувач673679

2
@DeadMG: Хочете поділитися MakeEqualityфрагментом? Зокрема об'єкт, повернутий за допомогою цієї функції. Виглядає дуже цікаво.
Deathicon

3

Перш за все, тут відбуваються різні речі:

  • розділення списку голих символів на лексеми
  • розпізнавання цих маркерів (визначення ключових слів, літералів, дужок, ...)
  • перевірка загальної структури граматики

Як правило, ми очікуємо, що лексер зробить усі 3 кроки за один крок, однак останній по суті складніше і є деякі проблеми з автоматизацією (докладніше про це пізніше).

Найдивовижніший лексер, про який я знаю, - Boost.Spirit.Qi . Він використовує шаблони виразів для генерування ваших лексерних виразів, і коли він звик до синтаксису, код виглядає дійсно акуратним. Він збирається дуже повільно (важкі шаблони), тому краще виділити різні частини у виділені файли, щоб уникнути їх перекомпіляції, коли вони не торкнулися.

У роботі є деякі підводні камені, і автор укладача Epoch пояснює, як він досяг 1000-кратного прискорення шляхом інтенсивного профілювання та дослідження того, як працює Ци в статті .

Нарешті, існує також генерований код із зовнішніх інструментів (Yacc, Bison, ...).


Але я пообіцяв написати, що не так у автоматизації перевірки граматики.

Якщо ви скажете, наприклад, Clang, то зрозумієте, що замість того, щоб використовувати згенерований аналізатор і щось на зразок Boost.Spirit, замість цього вони мали намір перевірити граматику вручну за допомогою загальної техніки Descent Parsing. Невже це здається відсталим?

Насправді є дуже проста причина: відновлення помилок .

Типовий приклад для C ++:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Помітили помилку? Відсутня напівкрапка відразу після оголошенняFoo .

Це звичайна помилка, і Кланг акуратно відновлюється, розуміючи, що вона просто відсутня і voidне є примірникомFoo а частиною наступної декларації. Це дозволить уникнути важких для діагностики критичних повідомлень про помилки.

Більшість автоматизованих інструментів не мають (принаймні очевидних) способів вказувати ці ймовірні помилки та способи їх виправлення. Часто для відновлення потрібен невеликий синтаксичний аналіз, тому це далеко не очевидно.


Отже, у використанні автоматизованого інструменту є компроміс: ви швидко отримуєте свій аналізатор, але він менш зручний для користувачів.


3

Оскільки ви хочете дізнатися, як працюють лексери, я припускаю, що ви насправді хочете знати, як працюють генератори лексерів.

Генератор лексерів приймає лексичну специфікацію, яка є переліком правил (пари регулярних виразів-лексем) і генерує лексеру. Потім отриманий лексер може перетворити вхідний (символьний) рядок у рядок лексеми відповідно до цього списку правил.

Метод, який найчастіше використовується в основному, полягає в перетворенні регулярного виразу в детерміновані кінцеві автомати (DFA) через недетерміновані автомати (NFA), а також кілька деталей.

Детальний посібник щодо здійснення цієї трансформації можна знайти тут . Зауважте, що я сам цього не читав, але це виглядає досить непогано. Крім того, майже в будь-якій книзі про побудову компілятора буде показано це перетворення в перших кількох главах.

Якщо вас цікавлять слайди лекцій курсів з даної теми, без сумніву, нескінченна кількість їх із курсів з побудови компілятора. З мого університету ви можете знайти такі слайди тут і тут .

Є ще кілька речей, які зазвичай не використовуються в лексемах або не обробляються текстами, але все-таки є досить корисними:

По-перше, поводження з Unicode дещо нетривіально. Проблема полягає в тому, що вхід ASCII має ширину лише 8 біт, а це означає, що ви можете легко мати таблицю переходу для кожного стану в DFA, оскільки у них є лише 256 записів. Однак для Unicode шириною 16 біт (якщо ви використовуєте UTF-16) потрібно 64 кб таблиці для кожного запису в DFA. Якщо у вас складні граматики, це може почати займати досить багато місця. Заповнення цих таблиць також починає забирати зовсім небагато часу.

Крім того, ви можете генерувати інтервальні дерева. Дерево діапазону може містити, наприклад, кортежі ('a', 'z'), ('A', 'Z'), що набагато ефективніше пам'яті, ніж повна таблиця. Якщо ви підтримуєте інтервали, що не перетинаються, для цього можна використовувати будь-яке збалансоване двійкове дерево. Час виконання лінійно в кількості бітів, необхідних для кожного символу, тому O (16) у випадку Unicode. Однак у кращому випадку, як правило, це буде зовсім трохи менше.

Ще одне питання полягає в тому, що лексери, як зазвичай генеруються, насправді мають найгірший квадратичний показник. Хоча таке гірше поведінка не часто зустрічається, воно може вас вкусити. Якщо ви зіткнулися з проблемою і хочете її вирішити, тут можна знайти документ, що описує, як досягти лінійного часу .

Ймовірно, ви хочете мати можливість описувати регулярні вирази в рядковій формі, як вони зазвичай з’являються. Однак аналіз цих описів регулярних виразів на NFA (або, можливо, спочатку рекурсивну проміжну структуру) є дещо проблемою з курячим яйцем. Для розбору описів регулярних виразів дуже підходить алгоритм Shunting Yard. У Вікіпедії, здається, є велика сторінка алгоритму .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.