Недавно я пытался реализовать простейший токенизатор, используя 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
, но я не знаю почему.
Итак, у меня есть несколько вопросов:
- Что я делаю неправильно? (Я знаю, что это не в стиле stackoverflow, но я боролся с этим несколько дней, и мне не с кем посоветоваться). Я безуспешно пытался сохранить фактический комментарий в виде строки внутри классов
SingleLineComment
иWhitespace
. Я подозреваю, что это из-за того, что в парсере пропущены строки комментариев и пробелов, есть ли способ обойти это? - Каков наилучший подход к синтаксическому анализу гетерогенных структур?
- Стоит ли использовать для этой задачи какие-то специализированные библиотеки (т.е. следует использовать класс
grammar
илиspirit::lex
, однако в версии x3 таких вещей нет) - Есть ли примеры токенизаторов (я смотрел Руководство по началу работы с 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());
}
Спасибо за чтение, с нетерпением жду ответов!