Написание лексера на C ++

18

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

Я посмотрел в Интернете, и все говорят, чтобы использовать генератор лексера, как лекс. Я не хочу этого делать, я хочу написать лексер от руки.

rightfold
источник
Хорошо, почему lex не подходит для твоих целей?
CarneyCode
13
Я хочу узнать, как работают лексеры. Я не могу сделать это с помощью генератора лексеров.
rightfold
11
Лекс генерирует отвратительный С-код. Тот, кто хочет достойного лексера, не использует Лекса.
DeadMG
5
@Giorgio: сгенерированный код - это код, с которым вы должны взаимодействовать, например, с отвратительными не потоко-безопасными глобальными переменными, и это код, чьи ошибки NULL-завершения заканчиваются в вашем приложении.
DeadMG
1
@ Джорджио: Вам когда-нибудь приходилось отлаживать вывод кода Лексом?
Mattnz

Ответы:

7

Имейте в виду , что каждый конечный автомат соответствует регулярному выражению, которое соответствует структурированной программы с использованием ifи whileотчетности.

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

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

или регулярное выражение:

digit digit*

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

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

Лично я всегда пишу лексеры, используя последние, потому что ИМХО это не менее понятно, и нет ничего быстрее.

Майк Данлавей
источник
Я думаю, что если регулярное выражение становится очень сложным, то и соответствующий код. Вот почему генератор лексеров хорош: я обычно сам кодировал бы лексер, если язык очень прост.
Джорджио
1
@ Джорджио: Может быть, дело вкуса, но я построил много парсеров таким образом. Лексеру не нужно ничего обрабатывать, кроме цифр, знаков препинания, ключевых слов, идентификаторов, строковых констант, пробелов и комментариев.
Майк Данлавей
Я никогда не писал сложный парсер, и все написанные мной лексеры и парсеры также были написаны вручную. Мне просто интересно, как это масштабируется для более сложных регулярных языков: я никогда не пробовал, но я представляю, что использование генератора (такого как lex) было бы более компактным. Я признаю, что у меня нет опыта работы с lex или другими генераторами, кроме некоторых игрушечных примеров.
Джорджио
1
Там будет строка, к которой вы добавите *pc, верно? Как while(isdigit(*pc)) { value += pc; pc++; }. Затем после }вы конвертируете значение в число и назначаете его токену.
rightfold
@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, и пару операторов в стиле регулярных выражений, например, *для нуля или более, epsчтобы означать «соответствовать чему-либо» и optозначать «соответствовать». ничего, кроме как потреблять это ". Библиотека полностью универсальна и основана на итераторах. Материал MakeEquality - это простой тест на равенство *itи переданное значение, а MakeRange - простой <= >=тест.

В конце концов, я планирую перейти от отслеживания к прогнозированию.

DeadMG
источник
2
Я видел несколько лексеров, которые просто читали следующий токен по запросу парсера. Ваш, кажется, проходит через весь файл и составляет список токенов. Есть ли какое-то особое преимущество этого метода?
user673679
2
@DeadMG: Хотите поделиться MakeEqualityфрагментом? В частности, объект, возвращаемый этой функцией. Выглядит очень интересно.
Deathicon
3

Прежде всего, здесь происходят разные вещи:

  • разбить список голых персонажей на токены
  • распознавание этих токенов (определение ключевых слов, литералов, скобок, ...)
  • проверка общей грамматической структуры

Как правило, мы ожидаем, что лексер выполнит все 3 шага за один раз, однако последний по своей природе более сложен, и есть некоторые проблемы с автоматизацией (подробнее об этом позже).

Самый удивительный лексер, о котором я знаю, это Boost.Spirit.Qi . Он использует шаблоны выражений для генерации выражений лексера, и когда-то привыкнув к его синтаксису, код кажется действительно аккуратным. Хотя он компилируется очень медленно (тяжелые шаблоны), поэтому лучше выделять различные части в выделенных файлах, чтобы не перекомпилировать их, когда они не были затронуты.

В производительности есть некоторые подводные камни, и автор компилятора Epoch объясняет, как он получил ускорение в 1000 раз благодаря интенсивному профилированию и исследованию работы Qi в статье .

Наконец, есть также сгенерированный код с помощью внешних инструментов (Yacc, Bison, ...).


Но я обещал написать, что было не так с автоматизацией проверки грамматики.

Например, если вы посмотрите на Clang, вы поймете, что вместо использования сгенерированного парсера и чего-то вроде Boost.Spirit вместо этого они намеревались проверять грамматику вручную, используя общую технику анализа спуска. Конечно, это кажется отсталым?

На самом деле, есть очень простая причина: восстановление после ошибок .

Типичный пример в C ++:

struct Immediate { } instanceOfImmediate;

struct Foo {}

void bar() {
}

Заметили ошибку? Недостающая точка с запятой сразу после объявления Foo.

Это распространенная ошибка, и Clang аккуратно восстанавливается, понимая, что он просто отсутствует и 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. В Википедии, кажется, есть обширная страница об алгоритме .

Алекс тен Бринк
источник