Не работает токенизатор Boost Spirit x3 с аннотацией

Недавно я пытался реализовать простейший токенизатор, используя boost spirit x3. Проблема, с которой я сейчас борюсь, - это получение позиции каждого токена во входном потоке.

На официальном сайте есть хорошее руководство по аннотациям: https://www.boost.org/doc/libs/develop/libs/spirit/doc/x3/html/spirit_x3/tutorials/annotation.html. Однако у него есть некоторые ограничения: он в основном анализирует список идентичных (однородных) сущностей, хотя в реальной жизни это часто не так.

Итак, я пытался создать токенизатор с двумя сущностями: пробелом (последовательность пробелов) и однострочным комментарием (начинается с //, продолжается до конца строки).

См. Минимальный пример кода в конце вопроса.

Однако я получаю ошибки при попытке получить позицию любого из токенов. После некоторой отладки я обнаружил, что annotate_position::on_success дескриптор выводит T тип как boost::spirit::x3::unused_type, но я не знаю почему.

Итак, у меня есть несколько вопросов:

  1. Что я делаю неправильно? (Я знаю, что это не в стиле stackoverflow, но я боролся с этим несколько дней, и мне не с кем посоветоваться). Я безуспешно пытался сохранить фактический комментарий в виде строки внутри классов SingleLineComment и Whitespace. Я подозреваю, что это из-за того, что в парсере пропущены строки комментариев и пробелов, есть ли способ обойти это?
  2. Каков наилучший подход к синтаксическому анализу гетерогенных структур?
  3. Стоит ли использовать для этой задачи какие-то специализированные библиотеки (т.е. следует использовать класс grammar или spirit::lex, однако в версии x3 таких вещей нет)
  4. Есть ли примеры токенизаторов (я смотрел Руководство по началу работы с Boost.Spirit?, но он немного устарел)? На данный момент я думаю, что документации недостаточно, чтобы сразу начать писать что-то, и я подумываю написать токенизатор вручную. То, что рекламировалось как простая библиотека get set go, оказалось сложной связкой едва задокументированных шаблонов, которые я не совсем понимаю.

Вот минимальный пример кода:

#include <string>
#include <iostream>
#include <functional>
#include <vector>
#include <optional>
#include <variant>

#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/home/x3/support/ast/position_tagged.hpp>

using namespace std;
namespace x3 = boost::spirit::x3;

struct position_cache_tag;

// copy paste from boost documentation example
struct annotate_position
{
    template <typename T, typename Iterator, typename Context>
    inline void on_success(Iterator const &first, Iterator const &last, T &ast, Context const &context)
    {
        auto &position_cache = x3::get<position_cache_tag>(context).get();
        position_cache.annotate(ast, first, last);
    }
};

struct SingleLineComment : public x3::position_tagged
{
    // no need to store actual comment string,
    // since it is position tagged and
    // we can then find the corresponding
    // iterators afterwards, is this right?
};
struct Whitespace : public x3::position_tagged
{
    // same reasoning
};
// here can be another token types (e.g. MultilineComment, integer, identifier etc.)

struct Token : public x3::position_tagged
{
    // unites SingleLineComment and Whitespace
    // into a single Token class

    enum class Type
    {
        SingleLineComment,
        Whitespace
    };

    std::optional<Type> type; // type field should be set by semantic action
    // std::optional is kind of reinsurance that type will be set

    std::optional<std::variant<SingleLineComment, Whitespace>> data;
    // same reasoning for std::optional
    // this filed might be needed for more complex
    // tokens, which hold additional data
};

// unique on success hook classes
struct SingleLineCommentHook : public annotate_position
{
};
struct WhitespaceHook : public annotate_position
{
};
struct TokenHook : public annotate_position
{
};

// rules
const x3::rule<SingleLineCommentHook, SingleLineComment> singleLineComment = "single line comment";
const x3::rule<WhitespaceHook, Whitespace> whitespace = "whitespace";
const x3::rule<TokenHook, Token> token = "token";

// rule definitions
const auto singleLineComment_def = x3::lit("//") >> x3::omit[*(x3::char_ - '\n')];
const auto whitespace_def = x3::omit[+x3::ascii::space];

BOOST_SPIRIT_DEFINE(singleLineComment, whitespace);

auto _setSingleLineComment = [](const auto &context) {
    x3::_val(context).type = Token::Type::SingleLineComment;
    x3::_val(context).data = x3::_attr(context);
};
auto _setWhitespace = [](const auto &context) {
    x3::_val(context).type = Token::Type::Whitespace;
    x3::_val(context).data = x3::_attr(context);
};

const auto token_def = (singleLineComment[_setSingleLineComment] | whitespace[_setWhitespace]);

BOOST_SPIRIT_DEFINE(token);

int main()
{
    // copy paste from boost documentation example
    using iterator_type = std::string::const_iterator;
    using position_cache = boost::spirit::x3::position_cache<std::vector<iterator_type>>;

    std::string content = R"(// first single line comment

// second single line comment

    )";
    // expect 4 tokens: comment -> whitespace -> comment -> whitespace
    position_cache positions{content.cbegin(), content.cend()};

    std::vector<Token> tokens;
    const auto parser = x3::with<position_cache_tag>(std::ref(positions))[*token];

    auto start = content.cbegin();
    auto success = x3::phrase_parse(start, content.cend(), parser, x3::eps(false), tokens);
    success &= (start == content.cend());

    cout << boolalpha << success << endl;
    cout << "Found " << tokens.size() << " tokens" << endl;

    for (auto &token : tokens)
        cout << (token.type.value() == Token::Type::SingleLineComment ? "comment" : "space") << endl;

    // all good till this point

    // now I want to get a position
    // the following throws
    auto pos = positions.position_of(tokens.front());
}

Спасибо за чтение, с нетерпением жду ответов!


person Yurii A    schedule 07.01.2021    source источник
comment
Дух никогда не заводился. Это повышение производительности, если вы знаете, где находится золотая середина, ИМО. Не расстраивайтесь по поводу токенизации почерка. Spirit Lex никогда не был очень популярен, и даже с Ци было трудно удержать золотую середину. Теоретически это было предназначено для повышения производительности за счет сокращения поиска с возвратом, но обычно это усложняет правила до такой степени, что это не имеет значения. Я здесь, если вам нужна помощь, чтобы справиться с первоначальной головной болью. Я также могу сделать быстрый обзор, чтобы предсказать, соответствует ли X3 вашему варианту использования.   -  person sehe    schedule 07.01.2021
comment
Только что вспомнил, что раньше я делал дополнительную документацию / копался по обработке ошибок и тегам позиции: stackoverflow.com/a/61732124/85371.   -  person sehe    schedule 08.01.2021


Ответы (1)


Кажется, что on_success не происходит, когда задействованы семантические действия.

Фактически, вы избыточно помечаете узлы Ast и вариант.

Вы уже могли получить правильный результат для первого токена, например,

auto pos = positions.position_of(
    std::get<SingleLineComment>(tokens.front().data)));

Это явно не очень удобно из-за необходимости переключения статического типа.

Вот очень упрощенный вариант:

Live On Compiler Explorer

#include <iostream>
#include <iomanip>
#include <variant>
#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/home/x3/support/ast/position_tagged.hpp>
namespace x3 = boost::spirit::x3;

struct SingleLineComment{};
struct Whitespace       {};

using Variant = std::variant<SingleLineComment, Whitespace>;

struct Token : Variant, x3::position_tagged {
    using Variant::Variant;
};

namespace {
    struct position_cache_tag;
    namespace Parser {
        struct annotate_position {
            template <typename T, typename Iterator, typename Context>
                inline void on_success(Iterator first, Iterator last, T &ast, Context const &context) const {
                    auto &position_cache = x3::get<position_cache_tag>(context);
                    position_cache.annotate(ast, first, last);
                }
        };

        // unique on success hook classes
        template <typename> struct Hook {}; // no annotate_position mix-in
        template <> struct Hook<Token> : annotate_position   {};

        template <typename T>
        static auto constexpr as = [](auto p, char const* name = typeid(decltype(p)).name()) {
            return x3::rule<Hook<T>, T> {name} = p;
        };

        // rule definitions
        auto singleLineComment = as<SingleLineComment>("//" >> x3::omit[*(x3::char_ - x3::eol)]);
        auto whitespace        = as<Whitespace>       (x3::omit[+x3::ascii::space]);
        auto token             = as<Token>            (singleLineComment | whitespace, "token");
    }
}

int main() {
    using It             = std::string::const_iterator;
    using position_cache = x3::position_cache<std::vector<It>>;

    std::string const content = R"(// first single line comment

// second single line comment

    )";
    position_cache positions{content.begin(), content.end()};

    auto parser = x3::with<position_cache_tag>(positions)[*Parser::token];

    std::vector<Token> tokens;
    if (parse(begin(content), end(content), parser >> x3::eoi, tokens)) {
        std::cout << "Found " << tokens.size() << " tokens" << std::endl;

        for (auto& token : tokens) {
            auto pos = positions.position_of(token);
            std::cout
                << (token.index() ? "space" : "comment") << "\t"
                << std::quoted(std::string_view(&*pos.begin(), pos.size()))
                << std::endl;
        }
    }
}

Печать

Found 4 tokens
comment "// first single line comment"
space   "

"
comment "// second single line comment"
space   "

    "
person sehe    schedule 07.01.2021
comment
Обратите внимание на очень тонкое удаление std::ref с помощью with<>. Это было незначительное улучшение (около 1,61?). К счастью, семантика ссылок теперь используется по умолчанию. - person sehe; 07.01.2021
comment
спасибо, теперь я знаю в чем дело. Я видел ваш другой ответ о тегах позиций ранее, но заметил отсутствие семантических действий и спросил снова. Спасибо за помощь и время :) - person Yurii A; 08.01.2021
comment
У меня есть еще один вопрос: нужна ли функция as вместо того, чтобы вручную определять правила для каждого класса? Пытаюсь переписать таким образом и получаю ошибку компиляции, а именно BOOST_SPIRIT_DEFINE undefined for this rule при компиляции token правила @sehe - person Yurii A; 08.01.2021
comment
Нет необходимости в as<>. Я просто ленив и предпочитаю такие вещи. В любом случае я не использовал BOOST_SPIRIT_DEFINE. Если вы этого хотите, вы можете: конечно же лениться. - person sehe; 08.01.2021
comment
Если вы все еще хотите понять, почему вы получили значение BOOST_SPIRIT_DEFINE undefined для этого правила, мы можем продолжить это в чате, и вы можете показать код. Или можете задать другой вопрос :) - person sehe; 08.01.2021