- El crate
regexde Rust, tras años de reescritura, expuso gran parte de sus motores internos como la API de un crate versionado aparte,regex-automata, y la transición se completó enregex 1.9 - La reescritura partió de la dificultad para combinar estrategias de búsqueda, la complejidad de probar cada motor interno, la demanda de una API multipatrón más granular que
RegexSety la necesidad de compartir DFA completamente compilados regex-automataorganiza el procesamiento de expresiones regulares con el flujoAst → Hir → extracción de literales/Prefilter → Thompson NFA → PikeVM·BoundedBacktracker·DFA one-pass·DFA·DFA lazy → meta engine- El rendimiento se obtiene usando primero, siempre que sea posible, la búsqueda de literales y motores de la familia DFA, y complementándolos con
PikeVM, bounded backtracker o DFA one-pass cuando hay grupos de captura o casos excepcionales - La API pública y las abstracciones reutilizables facilitaron las pruebas y la experimentación, pero aumentaron la cantidad de código, el tamaño del binario y el tiempo de compilación; los DFA completamente compilados quedaron como opt-in y
regex-litese agregó como alternativa ligera
Reescritura de regex y publicación de regex-automata
- El
regexcrate de Rust fue reescrito durante varios años para mejorar su composición interna y facilitar la incorporación de optimizaciones, manteniendo la corrección - En ese proceso se creó
regex-automata, que expone como una API separada una parte considerable de la implementación interna del crateregex regex-automatase presenta como el primer caso en que el interior de una biblioteca de expresiones regulares se publica a este nivel como una biblioteca versionada aparteregex 1.9se lanzó el 5 de julio de 2023 y completó esta reescritura- Está dirigido a programadores de Rust y a personas interesadas en implementar motores de expresiones regulares basados en autómatas finitos, y presupone experiencia con expresiones regulares
Problemas antes de la reescritura
- El crate
regexexistente seguía la tradición de RE2 y usaba internamente varias estrategias de búsqueda, pero esas estrategias se fueron agregando de forma orgánica y se volvieron difíciles de combinarPikeVM, al haber sido diseñado como la primera estrategia, carecía del manejo necesario para iniciar e interrumpir búsquedas sobre subslices al combinarlo con un DFA lazy- Era difícil inferir qué estrategia se usaba para cada expresión regular
- Varias expresiones
matchimplementaban repetidamente la misma lógica, lo que facilitaba que quedaran desincronizadas - Incluso en expresiones regulares donde bastaba con usar Aho-Corasick, podía crearse innecesariamente un Thompson NFA que no se usaría
- Probar los motores internos también era complicado
- La API pública parece un único motor de expresiones regulares, pero por dentro hay varias estrategias que deben comportarse igual ante la misma entrada
- Antes de
regex 1.9, las estrategias internas no formaban parte de la API pública, por lo que era difícil probar cada motor de manera independiente - Las pruebas existentes tenían una estructura cercana a un hack: dependían de exponer APIs internas, implementaciones de
Fromno documentadas, macros y targets de prueba específicos por motor
- Varias solicitudes de APIs de nicho también eran difíciles de encajar en la superficie de la API existente
RegexSetsolo indica qué patrones hicieron match en algún lugar del haystack, pero no proporciona offsets de matches ni offsets de grupos de captura- Se necesitaba una función para ejecutar búsquedas anchored sin insertar
^en el patrón - Se solicitaba una función para pasar directamente un mutable scratch space durante la búsqueda, sin sincronización interna
- También se pidió ejecutar expresiones regulares sobre haystacks no contiguos, como streams o ropes
- Publicar los componentes internos como un crate versionado aparte permite experimentar con APIs para expertos con un ciclo de breaking changes más rápido, sin complicar la API general de
regex
La frontera de abstracción creada por el DFA completamente compilado
- La motivación inicial de
regex-automatafue crear un DFA completamente compilado y serializado, y luego ofrecer un runtime mínimo que buscara mediante deserialización zero-copy - El primer
regex-automatase usó para crear los DFA de la implementación de algoritmos Unicode enbstr - Al construir el DFA, se necesitaban una estructura de datos NFA y un compilador similares a los del crate
regex; a medida que ese código se volvió más complejo, creció la necesidad de compartirlo - Al principio se consideró un crate separado como
regex-nfa, pero más código, incluido el proceso de determinization, podía compartirse entreregexyregex-automata - La frontera de abstracción estaba más cerca de un motor de expresiones regulares que de un “NFA”, y finalmente
regex-automatase redefinió como un conjunto de varios motores - El plan a largo plazo era poner todos los motores de expresiones regulares en
regex-automatay convertir el crateregexen un wrapper delgado encima de él
Revisar la estructura interna con regex-cli
regex-clies un programa mantenido en el repositorio del crateregexy proporciona acceso por línea de comandos a varias APIs deregex-syntax,regex-automatayregex- Se puede instalar con el siguiente comando
cargo install regex-cli
regex-cli debugpuede imprimir AST, HIR, literales, Thompson NFA, DFA one-pass, DFA dense, DFA sparse, entre otros- La expresión regular
.con Unicode activado crea un Thompson NFA mucho más complejo para manejar UTF-8 scalar values, mientras que(?-u:.), con Unicode desactivado, crea un NFA más simple regex-cli findpuede ejecutar búsquedas ad hoc, y con el meta engine también permite búsquedas multipatrón y salida de grupos de captura
Flujo de datos del procesamiento de expresiones regulares
- La cadena de patrón pasada a
Regex::newprimero se parsea comoAst Astse transforma enHirHirtiene menos detalles queAst, y el Unicode case folding y las referencias a Unicode character classes se expanden durante la transformación
- A partir de
Hirse crean dos cosas- Secuencias de literales usadas para optimizar la búsqueda
- Thompson
NFA
- El NFA se convierte en la base para crear varios motores
PikeVM: maneja todas las expresiones regulares parseables y reporta offsets de grupos de capturaBoundedBacktracker: reporta offsets de grupos de captura mediante bounded backtracking- DFA one-pass: reporta rápidamente offsets de grupos de captura en expresiones regulares restringidas
- DFA dense: es muy rápido, pero solo reporta el inicio y el fin del match completo; su construcción requiere, en el peor caso, tiempo y espacio
O(2^m) - DFA lazy: crea el DFA a partir del NFA durante la búsqueda; normalmente es tan rápido como un full DFA y evita el costo exponencial de construcción de un full DFA
- Estos motores y
Prefilterse combinan en un único meta regex engine, y el crateregexes un wrapper delgado sobre ese meta engine
Optimización de literales
- La extracción de literales es una optimización clave dentro de
regex- Por ejemplo, toda coincidencia de
(foo|bar|quux)(\s+\w+)empieza con uno defoo,baroquux
- Por ejemplo, toda coincidencia de
- Los literales son importantes porque los algoritmos de búsqueda para una sola cadena o unas pocas cadenas son muy rápidos
- Con instrucciones vectoriales se pueden procesar varios bytes del haystack a la vez
- Los algoritmos generales de matching con expresiones regulares son difíciles de acelerar de forma consistente de la misma manera
- Una búsqueda de substring bien optimizada a menudo era al menos un orden de magnitud más rápida que un motor general de expresiones regulares
- La extracción de literales es heurística
- Debe reducir la tasa de falsos positivos de las coincidencias candidatas
- También debe reducir el impacto del prefilter en la latencia total
- Ambas condiciones dependen del haystack, pero analizarlo antes de la búsqueda puede empeorar el tiempo total de búsqueda
- Una secuencia de literales no es un conjunto, sino una secuencia ordenada
- El crate
regexsigue semántica tipo Perl de leftmost-first, por lo que|no es conmutativo - De
sam|samwisesolo puede extraersesam, pero ensamwise|samse consideran ambos
- El crate
- Para la búsqueda de una sola cadena se usa el módulo
memmemdel cratememchr- El algoritmo principal es Two-Way, con tiempo
O(n)en el peor caso y espacio constante - Para needles y haystacks cortos se usa Rabin-Karp
- En
x86_64se aprovecha una variante SIMD genérica
- El algoritmo principal es Two-Way, con tiempo
- Para la búsqueda de múltiples cadenas, el algoritmo principal es Teddy, portado desde Hyperscan, y en algunos casos también se usa Aho-Corasick
Thompson NFA y optimización
- La estructura de datos central dentro del crate
regexes un Thompson NFA - Thompson’s construction crea un NFA a partir de una representación estructurada de la expresión regular en tiempo
O(m), dondemes proporcional al tamaño de la expresión regular después de expandir las repeticiones contadas - El NFA puede usarse directamente como motor de expresiones regulares, y también se usa como base para otros motores al convertirlo a otros tipos, como un DFA
- La optimización principal del nuevo compilador de NFA se concentra en reducir las transiciones epsilon
- Un Thompson NFA tiene buen tiempo de construcción, pero usa muchas transiciones epsilon
- El cálculo de epsilon closure puede generar costos repetidos durante la búsqueda o la construcción del DFA
- La optimización de estado
sparserepresenta varias transiciones de rango en un solo estado y reduce múltiples instruccionesSplitexistentes- Elimina transiciones epsilon innecesarias en expresiones regulares como
[A-Za-z0-9] - En la representación actual, por la indirección, puede afectar el caché y aumentar el uso de memoria heap
- Elimina transiciones epsilon innecesarias en expresiones regulares como
- La optimización de autómatas UTF-8 mínimos reduce mucho el tamaño del NFA en clases Unicode grandes
- En el antiguo NFA orientado a bytes,
\wpodía generar 3,564 estados - El nuevo compilador usa Daciuk’s algorithm para crear una estructura con muchos menos estados y cero transiciones epsilon
- La reducción del NFA inverso está desactivada por defecto por su costo en tiempo de compilación
- En el antiguo NFA orientado a bytes,
- La optimización de trie de literales compila alternaciones de literales como
zap|z|zapperyabc|xyzen un trie con estructura compartida para reducir transiciones epsilon- Para preservar la semántica leftmost-first, divide los chunks de transición en cada punto donde aparece una coincidencia
- Como trabajo futuro para NFA se están evaluando Glushkov NFA y el almacenamiento en una sola asignación contigua
- Glushkov NFA no tiene transiciones epsilon, pero su complejidad de tiempo de compilación es peor
- La asignación contigua podría aportar cache friendliness y serialización/deserialización zero-copy, pero puede aumentar la complejidad del código y el uso de
unsafe
Motores individuales de expresiones regulares
- Los motores de
regex-automatacomparten APIs similaresInput: configura el haystack, el rango de búsqueda, si es anchored y si hay early stopMatch: contiene el byte span coincidente y elPatternIDMatchError: representa un error en el que no se puede determinar el resultado de la búsqueda
-
PikeVM
- Soporta todas las funciones que
regex-syntaxpuede parsear y funciona con haystacks de cualquier longitud - Rastrea las posiciones de los grupos de captura y calcula los estados activos en lock-step, lo que garantiza tiempo
O(m * n)en el peor caso - Su principal debilidad es el rendimiento, porque debe rastrear muchos estados y posiciones de grupos de captura
- Tiene la característica única entre los motores de
regex-automatade no devolver errores durante la búsqueda
- Soporta todas las funciones que
-
BoundedBacktracker
- Usa backtracking sobre un Thompson NFA, pero emplea estado adicional para no volver a rastrear trabajo ya realizado
- Garantiza tiempo
O(m * n)en el peor caso, pero usa espacioO(m * n) - En experimentos aproximados, suele ser alrededor de 2 veces más rápido que
PikeVM - Puede fallar si la longitud del haystack y el tamaño de la expresión regular superan la visited capacity configurada
-
one-pass DFA
- Reporta offsets de grupos de captura muy rápido en NFA one-pass limitados
- Se considera la forma más rápida de reportar grupos de captura
- Solo soporta búsqueda anchored, y muchas expresiones regulares no son one-pass
- En modo Unicode, una expresión regular que no era one-pass por solapamiento de transiciones a nivel de byte puede volverse one-pass al desactivar Unicode
-
fully compiled DFA
- Consta de dos DFA, uno forward y uno reverse, para encontrar el final y el inicio de la coincidencia completa
- Su construcción toma tiempo y espacio
O(2^m)en el peor caso, y un dense DFA usa mucha memoria - El motor full DFA está desactivado por defecto en el crate
regexy debe habilitarse explícitamente con la featureperf-dfa-full - Funciona sin
Cachemutable y puede serializarse como bytes raw para usar el runtime de búsqueda incluso en entornos que solo tienencore
-
hybrid NFA/DFA, lazy DFA
- Es similar a un full DFA, pero construye la tabla de transiciones durante la búsqueda
- Si una transición ya calculada está en el caché, la reutiliza; si no, calcula solo esa transición con la powerset construction del NFA
- El tiempo de búsqueda en el peor caso es
O(m * n), y el espacio está limitado por la cache capacity definida al construirlo - En los casos comunes, la mayoría de estados y transiciones se cachean y se comporta como
O(n)en promedio; en la práctica, para muchas expresiones regulares muestra un rendimiento de búsqueda similar al full DFA - Si el caché se llena repetidamente y se vuelve ineficiente, devuelve un error, y en el meta engine normalmente se reintenta con otro motor
El rol del meta regex engine
- El
meta regex enginebusca agrupar varios motores en uno y ofrecer al llamador una API infallible - El llamador no tiene que crear y pasar manualmente un
Cacheen cada búsqueda- El meta engine administra internamente un pool de caché thread-safe
- Si se quiere evitar el costo de sincronización, también ofrece una API de menor nivel que permite pasar explícitamente un
Cache
regex::Regex,regex::RegexSet,regex::bytes::Regexyregex::bytes::RegexSetson todos wrappers delgados del meta engine- La estrategia interna del meta engine es, a grandes rasgos, la siguiente
- Si se puede resolver solo con búsqueda de substrings únicos o múltiples, sin motor de expresiones regulares, también evita construir el NFA
- Cuando es posible, extrae una secuencia de literales de prefijo y la usa como
Prefilter - Si el prefijo no es adecuado, intenta optimizaciones de reverse anchored, reverse suffix y reverse inner
- En los demás casos, hace fallback a una core strategy que incluye
PikeVM, bounded backtracker, one-pass DFA, lazy DFA y full DFA
- La estrategia completa se puede resumir en dos frases
- Buscar literales tanto como sea posible
- Evitar usar
PikeVMtanto como sea posible
- Las optimizaciones reverse suffix y reverse inner, si se aplican mal, pueden caer en el peor caso
O(m * n^2)respecto del tamaño del haystack- El meta engine detecta cuando un reverse scan intenta avanzar más allá del final del match de sufijo anterior y hace fallback a la core strategy para mantener la garantía de complejidad temporal
Diferencias con RE2
- El crate
regexy RE2 tienen muchas similitudes- RE2 también tiene un NFA equivalente a PikeVM, bitstate backtracker, one-pass NFA, lazy DFA y una estructura que combina varios motores
- De los motores anteriores, el que RE2 no tiene es el fully compiled DFA
- Las principales diferencias son las siguientes
- Además de leftmost-first, RE2 admite como opción semántica POSIX de leftmost-longest
- RE2 tiene soporte Unicode más limitado y no tiene una opción para usar
\w,\s,\dy\bcon definiciones Unicode - RE2 tiene soporte limitado para operaciones de conjuntos en character classes, aparte de union
- Es posible que el PikeVM de RE2 sea más eficiente en memoria
- Las optimizaciones de literales de RE2 son limitadas, mientras que el crate
regexrealiza más optimizaciones de literales - El lazy DFA de RE2 comparte la misma transition cache entre varios threads y requiere sincronización, mientras que el crate
regexexige una caché separada por thread y usa más memoria - El crate
regexpublicaregex-syntaxyregex-automatacomo bibliotecas versionadas por separado, pero RE2 no ofrece esto regex-automataadmite regex multi-pattern de primera clase en todos los motores y también puede reportar el match y los offsets de capture groups de cada patrón coincidente
Estrategia de pruebas y benchmarks
- La nueva estrategia de pruebas consiste en convertir los motores internos en APIs independientes de primera clase y probar cada motor directamente
- Todas las pruebas de expresiones regulares se especifican en archivos TOML
- El crate
regex-testlee las pruebas TOML y las convierte en una representación estructurada - Para cada configuración de motor hay un Rust unit test, que ejecuta todas las pruebas TOML aplicables a ese motor
- Como el Rust unit test framework no es extensible, se agregó una infraestructura propia basada en environment variables para filtrar pruebas específicas
- Solo
regex-automatatiene más de 450 documentation tests - Durante la preparación de
regex 1.9también se agregaron muchos targets de fuzz testing y, con ayuda de Addison Crump, se encontraron algunos bugs - Los benchmarks se publican con un regex barometer llamado
rebarrebarbenchmarkea no solo el crateregex, sino también varios motores de expresiones regulares- En 242 benchmarks,
regex 1.9es en promedio 1.5 veces más rápido queregex 1.7.3en tiempo de búsqueda - El tiempo para compilar expresiones regulares tuvo cierta regresión
- Como
1.8fue una transition release que incluyó parte del trabajo de transición, se usó1.7como base de comparación
Costos y alternativa ligera
- La reescritura consumió la mayor parte del tiempo libre del autor durante los últimos años, y proyectos como ripgrep no pudieron tener releases durante un tiempo
- Las abstracciones públicas reutilizables tienden a requerir más código que las abstracciones solo internas
- Como consecuencia, aumentan el tamaño del binario y el tiempo de compilación
- Al publicarse la API interna de motores con versionado separado, romper la API requiere un breaking change release adecuado de
regex-automata - Para mitigar los costos, se aplicaron dos medidas
- El motor fully compiled DFA queda desactivado por defecto y se deja como feature opt-in
- Se publicó
regex-litecomo un crate nuevo
regex-liteapunta a ser un reemplazo casi drop-in del crateregex, pero se enfoca en optimizar el tamaño del binario y el tiempo de compilación- Renuncia a funcionalidades relacionadas con Unicode y rendimiento
- Mantiene la garantía de complejidad temporal
O(m * n) - No tiene dependencias e incluye su propio parser de expresiones regulares, sin compartir código con el crate
regex
regex-litetodavía es una mitigación experimental, pero muestra que, incluso desactivando optimizaciones y funciones Unicode mediante features del crateregex, es difícil acercarse al tamaño de binario y al tiempo de compilación deregex-lite
1 comentarios
Opiniones de Hacker News
Solo le di una mirada rápida, pero el crate regex de Rust es realmente impresionante
BurntSushi ha hecho muchas cosas excelentes, pero el crate
regexde Rust es legendario, y que el ecosistema de Rust haya tenido desde hace mucho una biblioteca de expresiones regulares rápida y fácil de usar ha sido una gran bendición para la comunidadLa serie de artículos de Russ Cox sobre expresiones regulares también es excelente, y la usé como referencia durante un verano en el que construí un motor de expresiones regulares, justo cuando empecé a sentir que las expresiones regulares eran la intersección perfecta entre teoría y práctica
Los cambios más profundos de pruebas en este artículo también son interesantes, y siendo un crate clave del ecosistema, se agradece una explicación sobre un tema tan profundo
Las expresiones regulares a veces son difíciles de leer y se abusa de ellas para cosas como validar correos electrónicos, pero siguen siendo una de las herramientas más densas en casi cualquier lenguaje
Como libro práctico, solo conozco bien Mastering Regular Expressions de Jeffrey Friedl, y del lado teórico lo cubren los libros de compiladores; el Dragon Book también estaba bien desde la perspectiva de implementación. Me da curiosidad si alguien recomienda otros libros sobre expresiones regulares
La idea es construir un autómata finito no determinista para la expresión regular y luego recorrer el grafo dirigido resultante; si es posible llegar a un vértice que corresponda a un estado de aceptación, entonces hay coincidencia
Si usas expresiones regulares, este ejercicio ayuda a entender que hay menos magia de la que parece, y si imaginas pelotitas rebotando sobre el NFA, incluso los bugs de backtracking catastrófico con los que terminas topándote en producción adquieren un sentido físico
En relación con el artículo original, el último comentario de BurntSushi en https://github.com/rust-lang/regex/issues/822 agrega contexto útil a la sección sobre APIs de nicho https://blog.burntsushi.net/regex-internals/#problem-request.... Buscar varias expresiones regulares a la vez en un texto es muy complejo, pero muy útil, así que tengo curiosidad por ver qué construye la comunidad con este patrón
Por ejemplo, en formatos con un número fijo de campos como
header:field1,field2,field3\"data\"hash, o en formatos donde la mayoría de los elementos son opcionales, comosuite~split/test1,test2@opt1:opt2^hw1^hw2#flags1#flags2, herramientas básicas comosplitno alcanzan y una expresión regular encaja muy bienTambién por eso las expresiones regulares se vuelven difíciles de leer tan rápido. En una sola expresión se mezclan el delimitador entre campos, la validez de cada campo y cuáles campos son opcionales
En realidad son tres preocupaciones separadas, pero la mayoría de las APIs de expresiones regulares no permiten separarlas por etapas y solo aceptan una única cadena combinada
Por las restricciones del lenguaje y el estado de protección por copyright, tuve que implementar por mi cuenta la funcionalidad de RegEx, y a veces las expresiones regulares realmente se sienten como vudú
No sé con qué frecuencia se usan otros motores, pero si muchos lenguajes de programación usan PikeVM, entiendo por qué Google hizo su propio SO para servidores y recurrió a motores más rápidos en ciertos casos para ahorrarse aunque fuera unos cuantos ciclos de reloj
Sé bien que agregar apenas unos caracteres más a la cadena de búsqueda puede volver mucho más lento el pattern matching. El dicho de “cuida los centavos y los pesos se cuidan solos” aplica muy bien a RegEx y a los ciclos de reloj, y viendo las conversaciones de los 90 sobre procesar millones de registros por segundo, esto probablemente hizo muy ricos a algunos programadores
Sobre todo porque, entre dialectos y contextos, la forma de manejar comillas o terminar la expresión cambia demasiado; ya renuncié a memorizarlo y termino buscando ejemplos cada vez que lo necesito
En ActiveState me encargaron, junto con un colega recién graduado, crear un depurador de expresiones regulares para el editor Komodo
Contratamos al legendario experto en Perl Mark Jason Dominus para que metiera hooks en el motor de expresiones regulares de Perl, y expusimos esos hooks en la UI para que los usuarios pudieran ver la ejecución paso a paso
Hoy en día las herramientas web son mejores, pero en 2001 el Rx Debugger de Komodo era de vanguardia y además fue muy divertido trabajar en eso
Estaba trabajando con una red aislada, así que la gente que necesitaba usar la herramienta no podía acceder a sitios en línea, y bajo ningún diseño se permitía siquiera pensar en poner datos de trabajo en una herramienta online
Pero la mayoría del esfuerzo se concentra en herramientas en línea, y las offline son raras e insuficientes comparadas con algo como https://regex101.com/
Me pregunto si esto se puede usar también con listas y no solo con cadenas
Siempre me frustró que existan herramientas potentes para buscar y modificar listas de caracteres, pero en cuanto pasan a ser listas de números o fechas, todo eso desaparece
Por ejemplo, si quisiera encontrar todas las secuencias en una lista de fechas de intentos de inicio de sesión donde, después de 5 o más fallos, viene un éxito, con una regex sería sencillo, pero en la práctica hay que armar manualmente bucles, flags y listas temporales
Se podría convertir la lista a cadena, procesarla y luego revertirla, pero las desventajas son obvias. Aunque no fuera tan rápido como una regex basada en cadenas, no entiendo por qué no debería existir una regex para tipos de lista arbitrarios
También encontré un prototipo en Python que hice hace tiempo: https://github.com/boppreh/listregex
Es muy lento, pero como experimento de API quedó satisfactorio, y además ofrece herramientas que las regex no tienen, como inversión de patrones, intersección y emparejamiento
Hacer que un motor de regex así tenga un alfabeto genérico es forzar demasiado las cosas desde el principio. Sobre todo, es demasiado difícil hacerlo de una forma que no perjudique el diseño de la API ni el rendimiento en sus casos de uso principales
No es difícil hacer un motor de regex de ese tipo si no te importa el rendimiento. Por ejemplo, puedes tomar el crate
regex-liteque publiqué y volverlo tan genérico como quieras, y en el proceso te encontrarás con obstáculos interesantesTampoco es que esto no exista en absoluto. La gente ha intentado hacerlo[1]. Pero parece que suelen exagerar un poco su utilidad general y por eso normalmente no logran mucha tracción :-)
[1]: https://docs.rs/automata/latest/automata/trait.Alphabet.html
std::basic_regexde la biblioteca estándar de C++ intenta algo así al exponer una clase plantilla para tipos de carácter definidos por el usuario: https://en.cppreference.com/w/cpp/regex/basic_regexSe puede proporcionar una clase trait que defina el comportamiento necesario para un “carácter” definido por el usuario
Pero el rendimiento cae muchísimo, y probablemente funcione tan bien como meter objetos arbitrarios no textuales en un
std::basic_stringdefinido por el usuarioNo es imposible, pero la mayoría de los lenguajes no tienen una buena interfaz para eso
Uso Ripgrep todos los días para buscar cosas en código o archivos de texto, y cada vez que lo uso en Windows, Linux, Mac, VSCode o Vim, me siento agradecido
Es uno de esos programas que cambiaron mi vida y mi forma de trabajar
Cuando me veo obligado a usar
grep, siento que volví a una época en la que todo corría en una CPU de un solo núcleo y los datos estaban en discos duros mecánicos lentísimos PATA/IDEBurntSushi merece de sobra el respeto que se les da a los grandes programadores
ag, y antes de esoack, y todos intentaron ofrecer una interfaz mucho mejor que la simple degrepPor un problema de trabajo tuve que crear un
RegexSetcon más de 10 millones de regex muy largasNingún motor podía manejarlo con la configuración predeterminada, y el
RegexSetde Rust tampoco alcanzaba con los valores por defectoAun así, usar
regex-automatayregex-syntax, y leer el código, fue un material de aprendizaje muy valioso incluso en 2018Al final, el proyecto laboral tomó como modelo la API de Lucene, pero solo fue posible después de aprender los fundamentos con los crates de regex
El trabajo a futuro es hacer que los motores de regex escalen mejor con una mayor cantidad de patrones. Ahora mismo colapsarían mucho antes de llegar a 10 millones de regex, y ni siquiera es fácil estar seguro de que ese objetivo sea realmente posible
Aun así, claramente se puede mejorar respecto al estado actual
Claro, en búsqueda multipatrón, Hyperscan es prácticamente el patrón oro. Aunque no sé qué tan bien manejaría 10 millones de patrones
Hace tiempo probé el crate
regex-automata, y fue la única librería que pude usar en un editor de texto porque permitía acceso directo al DFA internoLas APIs de bibliotecas de regex normales esperan que la entrada sea una sola cadena continua, pero este enfoque es compatible con cualquier estructura de datos de texto
Me apareció este artículo mientras estaba escribiendo código con
regex-automata, y encima usando el release inicial0.2.0Parece que ya toca revisar otra vez las nuevas estructuras internas
Todavía no pude leer el artículo, pero se ve muy interesante y con un timing perfecto
Unos minutos después vi que la respuesta era más bien “puede ser”, pero al ser un release formal tal vez incluso me permita simplificar bastante el código
Otros 10 minutos después, fue bastante painless, y el nuevo método
Builder::patchfue una mejora totalPosdata: todavía me tienes bloqueado en todos tus repositorios de GitHub, y considerando que muchos crates se usan ampliamente, me parece un poco injusto. No recuerdo el incidente original. Parece que el crate regex en sí ahora está bajo la organización
rust-lang, pero todavía hay cosas con las que no puedo interactuarregex-automata0.2.0tenía una gran advertencia sobre eso, y recomendaba enfáticamente usar0.1: https://docs.rs/regex-automata/0.2.0/regex_automata/Yo tampoco recuerdo el incidente original. Bloqueo a mucha gente por varias razones, pero ya te desbloqueé
BioJulia publicó Automa.jl, un motor de expresiones regulares escrito completamente en Julia que puede insertar código arbitrario de Julia en tiempo de compilación
No es por restarle mérito a que
regexde Rust está mucho más avanzado que Automa, pero me cuesta estar de acuerdo con la afirmación de que es el primer caso de exponer el interior de una expresión regular como bibliotecaPor ejemplo, PCRE2 tiene soporte de “callout” que suena parecido a lo que se menciona: https://www.pcre.org/current/doc/html/pcre2callout.html
Cosas como
ragelore2ctambién han venido haciendo algo similarLo que dice este blog es tomar el interior de la propia biblioteca de expresiones regulares, convertirlo en una biblioteca aparte con versionado independiente y hacer posible que otros la combinen
En los backtrackers esto suele ser menos natural porque el motor normalmente solo tiene un backtracker, pero las bibliotecas basadas en autómatas a menudo combinan varios motores de distintas maneras
Aun así, incluso los backtrackers podrían exponer cosas que en la práctica no suelen exponer, como el parser de expresiones regulares o el AST