Все статьи / Руководство по lexertl

C++ библиотека lexertl во многом лучше GNU Flex. Она умеет создавать лексические анализаторы из правил в runtime, может обрабатывать unicode, и даже умеет генерировать код сканера на C++. Библиотека header-only и очень проста в установке.

Пример к статье доступен на github


Библиотека lexertl служит для разбора регулярных грамматик. Иными словами, с её помощью можно написать лексический анализатор (англ. lexer или scanner), который обработает входной текст и превратить его в поток токенов, который гораздо легче использовать в полноценном парсере.

Установка lexertl

lexertl является header-only библиотекой для C++, и для установки достаточно

  • выкачать исходный код из репозитория на github
  • добавить путь к каталогу с исходным кодом в пути поиска заголовков

После этих шагов создайте новый файл или проект на C++, добавьте туда простой пример кода и соберите:

#include "lexertl/generator.hpp"
#include "lexertl/lookup.hpp"
#include <iostream>

int main()
{
    lexertl::rules rules;
    lexertl::state_machine sm;

    rules.push("[0-9]+", 1);
    rules.push("[a-z]+", 2);
    lexertl::generator::build(rules, sm);

    std::string input("abc012Ad3e4");
    lexertl::smatch results(input.begin(), input.end());

    // Read ahead
    lexertl::lookup(sm, results);

    while (results.id != 0)
    {
        std::cout << "Id: " << results.id << ", Token: '" <<
            results.str () << "'\n";
        lexertl::lookup(sm, results);
    }

    return 0;
}

Пишем сканер выражений калькулятора с lexertl

У библиотеки есть Web-страница с документацией: www.benhanson.net/lexertl.html

Представленная ниже программа выводит номера и строки токенов для входного выражения с идентификаторами и числами. Ввод читается построчно из stdin, для выхода из программы достаточно ввести символ конца файла.

>velocity + 27 * accel / 19
Id: 2, Token: 'velocity'
Id: 3, Token: '+'
Id: 1, Token: '27'
Id: 5, Token: '*'
Id: 2, Token: 'accel'
Id: 6, Token: '/'
Id: 1, Token: '19'

Библиотека состоит лишь из заголовочных файлов и не требует компоновки. Конечный автомат для лексического анализа может быть собран в runtime. Поэтому реализация состоит всего лишь из одного файла:

#include "lexertl/generator.hpp"
#include "lexertl/iterator.hpp"
#include <boost/range/iterator_range.hpp>
#include <iostream>
#include <memory>

enum {
    // ID пользовательских токенов должны начинаться с 1,
    //  т.к. число 0 lexertl резервирует для ID "конец ввода".
    // Если один из ID равен нулю, программа аварийно завершится.
    TK_NUMBER = 1,
    TK_IDENTIFIER,
    TK_PLUS,
    TK_MINUS,
    TK_MULTIPLY,
    TK_DIVIDE,
};

// Создаёт детерминированный конечный автомат (DFA)
//  для лексического анализа грамматики калькулятора
std::unique_ptr<lexertl::state_machine> BuildCalcLexer()
{
    lexertl::rules rules;

    // Метод push добавляет правило
    rules.push("[0-9]+", TK_NUMBER);
    rules.push("[a-z]+", TK_IDENTIFIER);
    rules.push("\\+", TK_PLUS);
    rules.push("\\-", TK_MINUS);
    rules.push("\\*", TK_MULTIPLY);
    rules.push("\\/", TK_DIVIDE);

    // Метод skip возвращает специальный ID,
    //  который означает, что это совпадение игнорируется.
    // Так мы можем пропустить пробельные символы.
    rules.push("[ \t\r\n]+", rules.skip());

    auto lexer = std::make_unique<lexertl::state_machine>();
    lexertl::generator::build(rules, *lexer);

    return lexer;
}

// Возвращает диапазон для прохода слева направо,
//  который при запросе следующего элемента
//  получает его из строки `line` с помощью лексера `lexer`
decltype(auto) TokenizeLine(const lexertl::state_machine &lexer, std::string &line)
{
    lexertl::siterator begin(line.begin(), line.end(), lexer);
    lexertl::siterator end;

    // Непосредственно при вызове токенизации не происходит,
    //  такой подход называется lazy evaluation with generators.
    return boost::make_iterator_range(begin, end);
}

int main()
{
    auto lexer = BuildCalcLexer();

    while (std::cin)
    {
        std::string line;
        std::getline(std::cin, line);
        for (auto &token : TokenizeLine(*lexer, line))
        {
            std::cout << "Id: " << token.id << ", Token: '" << token.str() << "'\n";
        }
    }

    return 0;
}

Генерация C++ кода

Можно легко модифицировать предыдущий пример, чтобы сгенерировать исходный код функции, реализующей лексический анализ той же самой регулярной грамматики. Полученный код по-прежнему использует библиотеку lexertl, но уже не требует конструирования лексера во время выполнения.

Первым делом подключите два новых заголовка:

#include "lexertl/generator.hpp"
#include "lexertl/generate_cpp.hpp"

Затем в функцию BuildCalcLexer после вызова lexertl::generator::build добавьте всего две строки кода:

// Минимизируем детерминированный конечный автомат,
//  это занимает немного времени, но взамен ускорит работу сканера.
lexer->minimise();

// Генерируем table-driven лексер на C++,
// В качестве имени функции передаём LookupExprToken.
lexertl::table_based_cpp::generate_cpp("LookupExprToken", *lexer, false, std::cout);

Запустите изменённый пример, и при старте программа выведет в консоль C++ код функции lookupExprToken, который выглядит примерно так:

template<typename iter_type, typename id_type>
void LookupExprToken (lexertl::match_results<iter_type, id_type> &results_)
{
    // ...table-driven реализация разбора следующего токена...
}

Сгенерированный код можно поместить в файл “LookupExprToken.hpp”, и после этого для реализации лексера достаточно подключить заголовок и написать управляющую функцию. Полученная программа будет вести себя так же, как в предыдущем примере:

#include "lexertl/iterator.hpp"
#include "LookupExprToken.hpp"

int main()
{
    while (std::cin)
    {
        std::string line;
        std::getline(std::cin, line);

        // Заполняем буфер состояния.
        lexertl::smatch results(line.begin(), line.end());

        // Cчитываем первый токен
        LookupExprToken(results);
        while (results.id != 0)
        {
            std::cout << "Id: " << results.id << ", Token: '" << results.str() << "'\n";
            LookupExprToken(results);
        }
    }

    return 0;
}

Отладочный вывод структуры ДКА

Можно выполнить отладочную печать состояний и переходов созданного в lexertl ДКА.

Во-первых нужно подключить ещё один заголовок:

#include "lexertl/debug.hpp"

Затем в функцию BuildCalcLexer после вызова lexertl::generator::build добавьте вызов отладочной печати:

// Минимизируем детерминированный конечный автомат,
//  это занимает немного времени, но взамен уменьшит объём таблицы.
lexer->minimise();

// Выводим в stdout человекочитаемые таблицы состояний и переходов.
lexertl::debug::dump(*lexer, std::cout);