Первый выпуск scan, библиотеки разбора текста для C++ с построением конечного автомата при компиляции

Опубликован первый выпуск библиотеки scan (0.1.0), разбирающей текст в значения по шаблону, известному на этапе компиляции. Шаблон или формат записывается как аргумент шаблона C++ и превращается при компиляции в размеченный детерминированный конечный автомат (tagged deterministic finite automaton, TDFA), обход которого разворачивается по состояниям в код на этапе компиляции с использованием шаблонов C++. Результатом выполнения является значение запрошенного типа. Код проекта написан на C++23 и распространяется под лицензией GPLv3. import scan; // Совпадает ли вся строка. Каждая точка входа — ещё и адаптор диапазона. scan::match<«[a-z]+@[a-z.]+»>(address); address | scan::match<«[a-z]+@[a-z.]+»>; // С группами. const auto found = scan::match<«([0-9]+)-([a-z]+)»>(«42-abc»); found.get<1>().to_view(); // «42» // Начало строки, занятое шаблоном, и первое совпадение где угодно. scan::starts_with<«[a-z]+»>(«abc123»).whole().to_view(); // «abc» scan::search<«[0-9]+»>(«id=4210x»).to_view(); // «4210» // Все совпадения и куски между ними — ленивые представления. for (const auto& one : text | scan::search_all<«[a-z]+»>) { … } const auto fields = «a,bb,,ccc» | scan::split<«,»> | std::ranges::to<std::vector>(); // Значения, а не текст. struct row { int id; std::string_view name; }; const row one = scan::scan<«{},{[a-z]+}»>(line); // Список, сумма типов, вложенная форма. struct all { std::vector<int> values; std::variant<int, std::string_view> tail; }; const all got = scan::scan<«{{}{*,?}} {{[0-9]+}|{[a-z]+}}»>(«1,2,3 abc»); // Начало ввода и то, что от него осталось. const auto [value, rest] = scan::scan_prefix<«{},{}»>(line).take<point>(); // По записи за раз, из чего угодно. for (const row& one : scan::each<«{},{[a-z]+}\\n»>(text).of<row>()) { … }

Диапазон, который можно прочитать лишь однажды, читается без буферизации, поля собираются по мере поступления символов. Сколько символов придётся удержать, шаблон задаёт при компиляции; там, где такого числа нет, чтение отклоняется при компиляции. std::istringstream source(«set speed 42\\nset gain 7\\n»); source > std::noskipws; struct command { scan::held<16> name; int value; }; for (const command& one : scan::each<«set {[a-z]+} {[0-9]+}\\n»>(std::views::istream<char>(source)) .of<command>()) { … } Производительность.

Замеры проведены на Ryzen 9 9950X, clang 22.1.8 с libc++, -O3 -march=native,LTO. Тридцать две записи за проход, медиана семи проходов. «([a-z]+),([a-z]+),([a-z]+),([a-z]+),([a-z]+)» «alpha,bravo,charlie,delta,echo» scan::scan<f>.sentinel() 477 нс re2c 554 нс scan::scan<f> 659 нс CTRE 717 нс RE2 15986 нс те же пять полей, по двести букв каждое scan::scan<f>.sentinel() 57.8 нс re2c 637 нс CTRE 692 нс RE2 11751 нс «first.last@subdomain.example.com» распознавание, ничего не извлекается scan::match<p>.sentinel().scalar() 436 нс scan::match<p>.scalar() 545 нс re2c 1186 нс RE2 2616 нс CTRE 14339 нс Свёртка групп по ходу разбора

Типу сообщается, к какой из его групп относится очередной символ, и вычисление выполняется на месте: ни одна итерация цикла не сохраняется, ни одной подстроки не создаётся. struct tally { unsigned long value = 0; }; struct reading { tally number; std::string_view tail; }; template <> struct scan::scanner<tally> { static constexpr std::string_view pattern() { return R»(\\(((_+)(X|Y)*)*\\))»; } struct state_type { unsigned long total = 0; unsigned place = 0, marks = 0; }; static constexpr state_type begin_groups() { return {}; } static constexpr void opened_group(state_type& one, scan::group_at<0>) { one.place = 0; one.marks = 0; } static constexpr void closed_group(state_type& one, scan::group_at<0>) { unsigned long weight = 1; for (unsigned step = 1; step < one.place; ++step) weight *= 10; one.total += weight * one.marks; } static constexpr void push_group(state_type& one, scan::group_at<1>, char) { ++one.place; } static constexpr void push_group(state_type& one, scan::group_at<2>, char letter) { one.marks += letter == ‘Y’ ? 2u : 1u; } static constexpr tally finish_groups(state_type one) { return {one.total}; } }; const reading got = scan::scan<«value={}{[a-z]*}»>(«value=(__X_XX)abcdefgh»).of<reading>(); // got.number.value == 12, got.tail == «abcdefgh»

heap scan::scan<f>.scalar() написанное вручную scan::scan<f> 10 141 ns 41.4 ns 42.5 ns 170 ns 100 439 ns 341 ns 392 ns 1176 ns 1000 3314 ns 3170 ns 3797 ns 11887 ns
Контексты и аллокаторы

Любой объект вызывающей стороны — аллокатор, пул, арену, что угодно ещё — можно передать в тот вызов, который создаёт значение. Тип контекста при этом не теряется: состояние сканера и каждый его хук могут быть шаблонами по нему, так что в сканере не упомянут ни один тип вызывающей стороны. struct arena { std::pmr::memory_resource* where = nullptr; std::pmr::memory_resource* resource() const { return where; } }; struct numbers { std::pmr::vector<int> values; }; struct both { numbers left; numbers right; }; template <> struct scan::scanner<numbers> { static constexpr std::string_view pattern() { return «([0-9]+)(?:,([0-9]+))*»; } template <class Told> struct state { std::pmr::vector<int> values; int running = 0; }; static state<scan::default_context_t> begin_groups() { return {}; } template <class Told> requires requires(const Told& one) { one.resource(); } static state<Told> begin_groups(const Told& told) { return {std::pmr::vector<int>(told.resource()), 0}; } template <class Told, std::size_t Which> static void push_group(state<Told>& one, scan::group_at<Which>, char digit) { one.running = one.running * 10 + (digit — ‘0’); } template <class Told, std::size_t Which> static void closed_group(state<Told>& one, scan::group_at<Which>) { one.values.push_back(one.running); one.running = 0; } template <class Told> static numbers finish_groups(state<Told> one) { return {std::move(one.values)}; } }; std::pmr::monotonic_buffer_resource bytes; const arena mine{&bytes}; scan::scan<«{} {}»>(text).of<both>(mine); // один на оба места scan::scan<«{} {}»>(text).of<both>(mine, scan::default_context); // по одному на место scan::scan<«{} {}»>(text).of<both>({mine, scan::default_context}); // то же в скобках const both got = scan::scan<«{} {}»>(text).with(mine); // Контексту не обязательно быть своим типом: аллокатор доходит до того, // что строит чтение. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan<«{[a-z]+} {[a-z]+}»>(text).of<( std::pmr::polymorphic_allocator<>(&bytes));

Хук получает сам объект вызывающей стороны, а не копию, поэтому состояние может хранить его адрес: контекст живёт столько же, сколько вызов, а всё чтение происходит внутри этого вызова. Прочие особенности

  • Два слоя: слой шаблонов (match, starts_with, search, search_all, split) и слой форматов, где «{}» — поле, а значение места определяют поля самого типа.
  • Правило разрешения неоднозначности — leftmost-first, как в Perl, RE2 и CTRE.
  • Всё перечисленное работает и в константных выражениях.
  • Нет lookaround, обратных ссылок и свойств Unicode; шаблоны работают с байтами.
  • Не поддерживаются шаблоны, известные только во время выполнения.

Сборка

Собирается в clang и GCC. Модули C++ не обязательны: доступны заголовочные файлы (include/), которые генерирует из тех же модулей утилита demodulizer — каждый push в main пересобирает, линкует и коммитит их обратно. Единственная зависимость, Boost.PFR, не нужна при включённых binding packs из C++26. FetchContent_Declare(scan GIT_REPOSITORY https://github.com/j4niwzis/scan.git GIT_TAG v0.1.0) FetchContent_MakeAvailable(scan) target_link_libraries(mine PRIVATE scan::scan)

Источник: opennet.ru

Купить надежный хостинг для сайтов с защитой от DDoS, VPS VDS серверы 🔥 Купить надежный хостинг для сайтов с защитой от DDoS, VPS VDS серверы | ProHoster