Files
bions-rust/bion-regex/README.md
cloudion-labo 2556698dd3 🦀 bions-rust vague 1 : 6 briques build-your-own-x — on ne les rebuild plus jamais
Principe RS-7 : « dès qu'on build un truc, plus personne n'a à le rebuild —
la seule chose à faire est l'optimisation. » (nexus/RepoVerse)
- bion-vc      : horloges vectorielles + MvReg fork-visible (LA spec
                 xion-relativiste-v0 enfin codée — CRDT testé par permutations)
- bion-triplet : l'Adressage Génératif (gen_hash BLAKE3, coords, résidu ;
                 résidu vide quand déjà-su ; align décidable au bit)
- bion-tsoinlog: journal append-only rejouable (CRC32 maison, crash-recovery)
- bion-kv      : magasin clé-valeur bitcask (compaction atomique, tombstones)
- bion-regex   : moteur Thompson NFA linéaire (jamais exponentiel — Russ Cox)
- bion-git     : mini-git content-addressed (SHA-1 maison + vecteurs officiels,
                 branches divergentes = le fork visible)
129 tests verts, clippy 0 warning, doc française = chaque bion est un cours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:07:31 +00:00

127 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# bion-regex 🔤
Moteur d'expressions régulières **from scratch, std-only, zéro dépendance,
zéro unsafe** — LE classique *build-your-own-regex*, version **NFA de
Thompson** simulé par ensembles d'états. La brique regex du xerboxion :
construite une fois, plus jamais rebâtie, seulement optimisée (principe RS-7).
Référence fondatrice : Russ Cox, **« Regular Expression Matching Can Be
Simple And Fast »** — <https://swtch.com/~rsc/regexp/regexp1.html>. À lire
avec le code sous les yeux : ce crate en est une implémentation Rust fidèle
et commentée. Voir aussi la section *build-your-own-x* correspondante :
<https://github.com/codecrafters-io/build-your-own-x#build-your-own-regex-engine>.
## Quoi
Pipeline en trois étapes, un fichier par étape :
```
motif ──parse──▶ AST ──compile──▶ NFA ──simulation──▶ oui/non + position
src/parser.rs src/nfa.rs src/nfa.rs (ensembles d'états)
```
- `Regex::new(pattern) -> Result<Regex, Error>` — compile une fois pour toutes
- `is_match(text) -> bool` — le motif apparaît-il quelque part ?
- `find(text) -> Option<(usize, usize)>` — première occurrence,
*leftmost-longest*, offsets en **octets** (`&text[start..end]` = le match)
- Erreurs de syntaxe **localisées** (position en caractères) et en français
Langage supporté : littéraux Unicode · `.` (tout sauf `\n`) · `*` `+` `?` ·
`|` · groupes `(…)` (priorité seulement, pas de capture — choix assumé pour
garder l'API minuscule) · classes `[a-z]`, `[^…]`, `]`/`-` littéraux aux
positions classiques · `\d \D \w \W \s \S` · `\n \t \r` · métacaractères
échappés (`\.` `\(` …) · ancres `^` `$`.
## Pourquoi Thompson (et pas du backtracking)
C'est **le** point pédagogique du crate. Un moteur à backtracking (Perl,
PCRE, `re` de Python…) essaie les alternatives une par une et revient en
arrière : sur `a*a*a*…a*b` face à `aaaa…a` (sans `b`), il doit explorer
~2ⁿ découpages avant d'avouer l'échec — 30 `a` suffisent à le figer des
secondes, 40 des heures. C'est la racine des CVE « ReDoS ».
L'approche Thompson (1968), ressuscitée par l'article de Russ Cox :
1. **Compilation** : chaque nœud de l'AST devient un fragment de NFA d'au
plus UN état (`Char`, `Split`, assertions). Le NFA fait O(m) états pour
un motif de m caractères — jamais plus.
2. **Simulation par ensembles d'états** : on lit le texte UNE fois ; à chaque
caractère on maintient *l'ensemble de tous les états où le NFA pourrait
être* (≤ n états, dédupliqués). C'est la déterminisation « à la volée »,
sans matérialiser le DFA.
Résultat : **O(texte × motif) garanti, pour tout motif, tout texte**. Le
motif pathologique ci-dessus répond en microsecondes — c'est testé,
chronomètre à l'appui (`pathologique_a_star_reste_instantane`).
Les ancres `^`/`$` sont des états-assertions évalués pendant la fermeture
epsilon (elles ne consomment rien), donc elles marchent aussi au milieu
d'une alternance (`^début|fin$`).
## Exemple
```rust
use bion_regex::Regex;
let re = Regex::new(r"^\w+@\w+\.[a-z]+$").unwrap();
assert!(re.is_match("rs7@xerion.ch"));
let re = Regex::new(r"\d+").unwrap();
let texte = "il y a 285 bions";
let (s, e) = re.find(texte).unwrap();
assert_eq!(&texte[s..e], "285");
// Le piège qui tue un backtracker — instantané ici :
let patho = Regex::new(&format!("{}b", "a*".repeat(30))).unwrap();
assert!(!patho.is_match(&"a".repeat(30)));
```
## Le bug classique (vécu, puis testé)
Première version : la fermeture epsilon marquait les états `Split` comme
« vus » mais ne les démarquait pas entre deux caractères → les boucles
(`a+`, `(ab)*`) n'étaient traversables qu'**une seule fois** (`ab+c`
matchait `abc` mais pas `abbc`). Le fix : l'ensemble d'états garde la liste
de TOUS les états marqués (pas seulement les stables) pour tout démarquer au
`clear`. Si tu réimplémentes ce moteur, tu feras ce bug — le test
`plus_exige_au_moins_un` t'attend.
## Complément, pas doublon
Le core (`~/xerboxion-rt`) n'a aucun moteur de motifs ; les ploxions font du
`match exact only` (cf. tsoin engine / `config/ploxions.php`). `bion-regex`
est la brique qui manque : filtrage de tsoins, routes, validation d'entrées —
embarquable partout (std-only, compile en WASM sans rien changer).
## Comment l'optimiser (l'invitation au fork)
Le moteur est volontairement la version *simple et juste*. Pistes, par ordre
de rendement (toutes dans les articles suivants de Russ Cox,
[regexp2](https://swtch.com/~rsc/regexp/regexp2.html) et
[regexp3](https://swtch.com/~rsc/regexp/regexp3.html)) :
1. **`find` en un seul passage** — aujourd'hui `find` relance une simulation
ancrée par position de départ (O(n²·m) au pire). La VM de Pike attache la
position de départ à chaque « thread » : leftmost-longest en O(n·m).
2. **Captures** — même VM de Pike : chaque thread porte ses positions de
sous-groupes. C'est l'étape qui transforme `(…)` en vraies captures.
3. **Cache DFA à la RE2** — mémoïser les ensembles d'états rencontrés :
chaque caractère devient UN lookup de table (c'est ce que fait `grep`).
4. **Octets plutôt que chars** — compiler les classes Unicode en automate
sur les octets UTF-8 : plus de décodage à l'exécution.
5. **Littéraux préfixes**`memchr` sur le premier octet obligatoire avant
de lancer le NFA (l'optimisation qui rend `ripgrep` rapide).
L'API (`new` / `is_match` / `find` / `pattern`) ne bouge pas : optimiser =
remplacer l'intérieur, jamais casser l'extérieur.
## Tests
```
cargo test -p bion-regex # 34 tests : 12 unitaires + 20 intégration + 2 doc
```
Couvre : les pièges pathologiques (chronométrés), boucles vides imbriquées
`((a*)*)*`, classes négatives, `]`/`-` littéraux, ancres en alternance,
matchs vides, offsets Unicode, et huit erreurs de syntaxe localisées.