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

Обсуждение новостей, соответствующих тематике форума

Модератор: Модераторы разделов

Аватара пользователя
rssbot
Бот
Сообщения: 6000
ОС: gnu/linux

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

Сообщение rssbot »

Опубликован первый выпуск библиотеки 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)



Источник: https://www.opennet.ru/opennews/art.shtml?num=66327
(opennet.ru, мини-новости)
Спасибо сказали: