🦀 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>
This commit is contained in:
cloudion-labo
2026-08-16 01:07:31 +00:00
commit 2556698dd3
27 changed files with 6518 additions and 0 deletions

126
bion-regex/README.md Normal file
View File

@@ -0,0 +1,126 @@
# 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.