Primer lanzamiento de scan, una biblioteca de análisis de texto para C++ que construye un autómata finito en tiempo de compilación

Se ha publicado la primera versión de la biblioteca scan (0.1.0), que analiza el texto en valores según una plantilla conocida en el momento de la compilación. La plantilla o formato se escribe como un argumento de plantilla en C++ y se convierte en un autómata finito determinista etiquetado (tagged deterministic finite automaton, TDFA) durante la compilación. Su recorrido se despliega a través de los estados en el código en la etapa de compilación utilizando plantillas de C++. El resultado de la ejecución es un valor del tipo solicitado. El código del proyecto está escrito en C++23 y se distribuye bajo la licencia GPLv3. import scan; // Verifica si toda la cadena coincide. Cada punto de entrada es además un adaptador de rango. scan::match(address); address | scan::match; // Con grupos. const auto found = scan::match("42-abc"); found.get().to_view(); // "42" // Inicio de la cadena, ocupada por la plantilla, y la primera coincidencia en cualquier lugar. scan::starts_with("abc123").whole().to_view(); // "abc" scan::search("id=4210x").to_view(); // "4210" // Todas las coincidencias y fragmentos entre ellas son representaciones perezosas. for (const auto& one : text | scan::search_all) { … } const auto fields = "a,bb,,ccc" | scan::split | std::ranges::to(); // Valores, no texto. struct row { int id; std::string_view name; }; const row one = scan::scan(line); // Lista, suma de tipos, forma anidada. struct all { std::vector values; std::variant tail; }; const all got = scan::scan("1,2,3 abc"); // Inicio de la entrada y lo que queda de ella. const auto [value, rest] = scan::scan_prefix(line).take(); // Por registro a la vez, de cualquier cosa. for (const row& one : scan::each(text).of()) { … }

Un rango que solo se puede leer una vez se lee sin almacenamiento en búfer, los campos se recopilan a medida que llegan los caracteres. La cantidad de caracteres a mantener es determinada por la plantilla en la compilación; donde no existe tal cantidad, la lectura es rechazada en la compilación. std::istringstream source("set speed 42\\nset gain 7\\n"); source > std::noskipws; struct command { scan::held name; int value; }; for (const command& one : scan::each(std::views::istream(source)) .of()) { … } Rendimiento.

Las mediciones se realizaron en Ryzen 9 9950X, clang 22.1.8 con libc++, -O3 -march=native, LTO. Treinta y dos registros por pase, mediana de siete pases. «([a-z]+),([a-z]+),([a-z]+),([a-z]+),([a-z]+)» «alpha,bravo,charlie,delta,echo» scan::scan<f>.sentinel() 477 ns re2c 554 ns scan::scan<f> 659 ns CTRE 717 ns RE2 15986 ns los mismos cinco campos, doscientos caracteres cada uno scan::scan<f>.sentinel() 57.8 ns re2c 637 ns CTRE 692 ns RE2 11751 ns «first.last@subdomain.example.com» reconocimiento, no se extrae nada scan::match<p>.sentinel().scalar() 436 ns scan::match<p>.scalar() 545 ns re2c 1186 ns RE2 2616 ns CTRE 14339 ns Agrupación de grupos durante el análisis.

A cada tipo se le informa a qué grupo pertenece el siguiente símbolo, y el cálculo se realiza en su lugar: no se guarda ninguna iteración del ciclo, no se crea ninguna subcadena. struct tally { unsigned long value = 0; }; struct reading { tally number; std::string_view tail; }; template struct scan::scanner { 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) { one.place = 0; one.marks = 0; } static constexpr void closed_group(state_type& one, scan::group_at) { 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, char) { ++one.place; } static constexpr void push_group(state_type& one, scan::group_at, 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=(__X_XX)abcdefgh»).of(); // got.number.value == 12, got.tail == «abcdefgh»

heap scan::scan.scalar() escrito manualmente scan::scan 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
Contextos y asignadores

Cualquier objeto del llamador — un asignador, un pool, una arena, lo que sea — puede ser pasado a la llamada que crea un valor. El tipo de contexto no se pierde en este proceso: el estado del escáner y cada uno de sus hooks pueden ser plantillas a partir de él, de modo que ningún tipo del llamador se menciona en el escáner. struct arena { std::pmr::memory_resource* where = nullptr; std::pmr::memory_resource* resource() const { return where; } }; struct numbers { std::pmr::vector values; }; struct both { numbers left; numbers right; }; template struct scan::scanner { static constexpr std::string_view pattern() { return «([0-9]+)(?:,([0-9]+))*»; } template struct state { std::pmr::vector values; int running = 0; }; static state begin_groups() { return {}; } template requires requires(const Told& one) { one.resource(); } static state begin_groups(const Told& told) { return {std::pmr::vector(told.resource()), 0}; } template static void push_group(state& one, scan::group_at, char digit) { one.running = one.running * 10 + (digit - '0'); } template static void closed_group(state& one, scan::group_at) { one.values.push_back(one.running); one.running = 0; } template static numbers finish_groups(state one) { return {std::move(one.values)}; } }; std::pmr::monotonic_buffer_resource bytes; const arena mine{&bytes}; scan::scan(text).of(mine); // uno para ambos lugares scan::scan(text).of(mine, scan::default_context); // uno por lugar scan::scan(text).of({mine, scan::default_context}); // lo mismo entre paréntesis const both got = scan::scan(text).with(mine); // El contexto no tiene que ser su propio tipo: el asignador llega a construir la lectura. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan(text).of<(std::pmr::polymorphic_allocator(&bytes));

El hook recibe el objeto del llamador en sí, no una copia, por lo que el estado puede almacenar su dirección: el contexto vive tanto como la llamada, y toda la lectura ocurre dentro de esta llamada. Otras características

  • Dos capas: la capa de plantillas (match, starts_with, search, search_all, split) y la capa de formatos, donde «{}» es el campo y el valor del lugar lo determinan los campos del propio tipo.
  • La regla de resolución de ambigüedades es leftmost-first, como en Perl, RE2 y CTRE.
  • Todo lo mencionado también funciona en expresiones constantes.
  • No hay lookaround, referencias inversas ni propiedades de Unicode; las plantillas funcionan con bytes.
  • No se admiten plantillas que sean conocidas únicamente en tiempo de ejecución.

Compilación

Se compila en clang y GCC. Los módulos de C++ no son obligatorios: están disponibles los archivos de encabezado (include/) que genera la utilidad demodulizer a partir de los mismos módulos. Cada push a main recompila, vincula y los commitea de nuevo. La única dependencia, Boost.PFR, no es necesaria con los paquetes de binding activados de 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)

Fuente: opennet.ru

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster