Contribuer à Catnip
Sommaire
- Prérequis
- Setup rapide
- Setup manuel (étape par étape)
- 1. Créer le venv
- 2. Installer les dépendances de dev
- 3. Compiler l'extension Rust
- 4. Installer le package
- Vérifier l'installation
- Boucle de dev quotidienne
- Après modification de code Rust
- Après modification de la grammaire Tree-sitter
- Après modification de code Python uniquement
- Commandes de test
- Structure du projet
- Problèmes courants
- make compile échoue avec "can't find crate"
- Import error catnip._rs
- Tests qui échouent après modification d'opcodes
Prérequis
- Python >= 3.12 avec headers (
python3-dev/python3-devel) - Rust (stable, édition 2021) avec
cargo(rustup.rs) - uv (gestionnaire de packages Python) (docs.astral.sh/uv)
- libgmp-dev (requis par la crate
rugpour l'arithmétique précision arbitraire) - tree-sitter-cli (installé automatiquement par
make setup-dev)
# Debian/Ubuntu
sudo apt install python3-dev libgmp-dev
# Fedora/RHEL
sudo dnf install python3-devel gmp-devel
# macOS (Homebrew)
brew install gmp
Optionnel :
- Node.js + pnpm (pour CodeMirror et l'extension VSCode)
- fdfind (
fd-find) pour le tracking incrémental du makefile
Setup rapide
git clone <repo>
cd catnip
# Setup complet automatique (venv + deps + compile + install)
make setup
source .venv/bin/activate
C'est tout. make setup crée le venv, installe les dépendances, compile l'extension Rust, et installe le package
Python.
Vérifier que tout marche :
catnip -c "2 + 2" # → 4
make test-quick # Rust units + Python language (~10s)
Setup manuel (étape par étape)
Si make setup ne convient pas, ou pour comprendre chaque étape :
1. Créer le venv
make venv
source .venv/bin/activate
2. Installer les dépendances de dev
make setup-dev
Installe : pytest, ruff, setuptools-rust, tree-sitter-cli, et les autres dépendances de build. Ne compile rien.
3. Compiler l'extension Rust
make compile
C'est l'étape la plus longue (~2 min la première fois). Le résultat est un .so dans catnip/ que Python importe
directement.
Pour un build plus rapide en dev (incrémental, sans LTO, codegen-units élevé), profil fastdev :
make dev # raccourci de CATNIP_DEV=1 make compile
Le rebuild incrémental passe de ~3 min (release) à ~10 s. Les libs natives et les tests suivent le profil de la dernière
build (make test après make dev reste en fastdev).
4. Installer le package
make install
Installe le package Python en mode éditable + enregistre le serveur MCP.
Vérifier l'installation
# Extension Rust chargée ?
python -c "import catnip._rs; print('ok')"
# CLI fonctionnelle ?
catnip -c "list(1, 2, 3).[* 2]"
# Tests rapides
make test-quick
Boucle de dev quotidienne
Après modification de code Rust
CATNIP_DEV=1 make compile # Rebuild incrémental (~10-30s)
make test-quick # Validation rapide
Après modification de la grammaire Tree-sitter
make grammar-deps # Régénère parser.c + highlighters
make compile # Recompile avec le nouveau parser
make test # Tests complets
Après modification de code Python uniquement
Rien à recompiler, le package est installé en mode éditable. Lancer les tests directement :
make test
Commandes de test
| Commande | Quoi | Durée |
|---|---|---|
make test-rust-fast |
Tests unitaires Rust | ~5s |
make test-quick |
Rust units + Python language | ~10s |
make test |
Tests Python complets (VM) | ~25s |
make test-vm |
Tests Python mode VM (parallèle) | ~15s |
make test-ast |
Tests Python mode AST (parallèle) | ~15s |
make test-cfg |
Suite VM + AST au niveau 3 (tier CFG+SSA) | ~40s |
make lint-rust |
Clippy (-D warnings) sur tous les crates |
~30s |
make test-all |
Clippy + Rust + Python VM + AST + docs + ND | ~2 min |
make test-long |
Sweeps longs exclus de test-all |
~1 min |
make check-docs |
Assertions et exécution des snippets docs/ |
~15s |
make check-cat |
Parsing de tous les .cat suivis par git |
~5s |
make check-nd-parity |
Accord des trois exécuteurs sur le broadcast | ~15s |
make bench |
Mesure comparée des configurations du moteur | ~1 min |
Règle pratique : test-quick pendant le dev, test-all avant de proposer un changement.
Côté forme, make format-all passe les quatre formateurs — Rust, Python, JS, Markdown — et make format s'arrête aux
deux premiers. Le passage Markdown enchaîne dev/doc_format.py, qui normalise les caractères et la typographie, puis
mdformat, qui replie à 120 colonnes ; l'ordre compte, la première étape existant en partie pour que la seconde ne coupe
pas où il ne faut pas (voir META). Les deux couvrent docs/ et wip/, et l'ensemble est idempotent :
un second passage ne doit rien changer. S'il change quelque chose, c'est que les deux étapes se contredisent.
Il existe un troisième mode d'exécuteur, pytest --executor=standalone, qu'aucune cible ne lance. Il remplace
Catnip par CatnipStandalone, un adaptateur qui parle au Pipeline Rust sans passer par Registry, Executor ni
Context. Il n'est pas maintenu : 226 tests y échouent (relevé du 2026-07-31, contre 180 en juillet), dont ~157 parce
qu'ils interrogent une infrastructure que cet adaptateur n'expose pas — ledger de refcount, JIT, debugger. Ne pas le
lire comme une régression, et ne pas s'appuyer dessus pour valider un changement.
test-long lance ce qui est marqué #[ignore] pour raison de durée : aujourd'hui le proptest différentiel du tier
CFG en version 4096 cas, contre 256 dans test-rust. Les autres #[ignore] du dépôt sont des marqueurs de défauts
connus, pas des sweeps, et n'y sont donc pas — une cible qui les lancerait serait rouge par construction.
test-cfg rejoue la même suite au niveau d'optimisation 3 (pytest --optimize=3), qui ajoute le tier inter-blocs
CFG+SSA. Elle sert d'oracle différentiel : un test vert ici et dans test-vm/test-ast est un programme dont le
tier ne change pas le comportement observable. Un test qui affirme ce qu'est le niveau par défaut porte le marqueur
default_optimize et n'est pas repositionné.
Le mode ND process n'est réellement exercé que par catnip_rs/tests/run_nd_process.rs, que lance make test-run. Le
chemin Python, lui, ne le mesure pas : sans binaire worker à jour dans le venv — et make compile ne le reconstruit
pas, il faut build-bins puis install-bins — le scheduler retombe sur thread et le programme rend le bon résultat
sans rien dire. Un test Python qui pose pragma("nd_mode", "process") compare donc, la plupart du temps, un run
thread à un run thread. Le témoin est le spawn lui-même, pas la sortie : le test Rust fait pointer
CATNIP_WORKER_BIN sur un script qui enregistre son invocation.
Les prérequis de build-bins couvrent les sources de catnip_rs, catnip_tools, catnip_core, catnip_vm, la
grammaire générée, et celles propres à chaque binaire. La liste doit rester complète : make décide seul d'appeler cargo,
donc une source absente laisse le binaire périmé sans que le rerun-if-changed du build.rs soit consulté.
Les tests unitaires de catnip_vm construisent eux-mêmes les plugins natifs qu'ils chargent — catnip_hello,
catnip-io et catnip-sys sont membres du workspace mais dépendances d'aucun crate, donc rien dans un
cargo test -p catnip_vm ne les bâtit. Un cargo build par groupe, une fois par processus de test, produit les .so
que la suite dlopen. Deux conséquences : le premier run d'un arbre neuf paie cette compilation, les suivants sont des
no-op ; et les trois crates doivent rester membres du workspace. Retirer l'un d'eux rend rouges les tests qui le
chargent — de 9 à 53 selon le crate — avec package ID specification ... did not match any packages pour seul message,
émis par le cargo imbriqué et non par la suite.
check-docs et check-cat couvrent deux choses différentes : le premier exécute les pages de docs/, le second
parse les 150 fichiers .cat du dépôt, où qu'ils soient. Un exemple qui vit hors de docs/ n'est vu que par le
second — c'est ce trou qui a laissé une démo de coloration syntaxique écrite en syntaxe Python (while x > 0:) dormir
dans l'arbre.
check-docs lance deux outils, parce qu'ils ne regardent pas la même chose. check_doc_assertions.py compare : il
ne visite que les blocs portant un # → valeur et vérifie le résultat. check_examples.py exécute : il prend les
538 blocs des pages plus les 66 scripts de docs/examples/, et n'exige que l'absence d'erreur. Un snippet sans
assertion n'était donc contrôlé par rien, et le second outil ne tournait dans aucune cible — c'est ainsi que les deux
défauts de portée et de broadcast ND du 2026-07-29 ont vécu dans des exemples publiés.
Un snippet qui démontre une erreur n'a pas besoin de marqueur : un nom de type d'erreur en commentaire de fin de
ligne (return 42 # SyntaxError: 'return' outside function) suffit à déclarer l'attente, et la ligne réussissante
devient l'échec. La distinction porte sur la ligne, pas sur le fichier : le même nom dans une ligne entièrement
commentée reste de la prose, puisqu'elle décrit du code qui ne s'exécute pas. Les marqueurs explicites
(<!-- check: expect-error -->, <!-- check: no-check -->) restent nécessaires quand le bloc lève sans nommer quoi.
Le nom déclaré doit être celui qui sort : un bloc annoté # TypeError qui lève une NameError échoue, alors qu'il
passait tant que n'importe quelle erreur satisfaisait l'attente — c'est ainsi qu'un exemple de STRUCTURES.md a
longtemps échoué faute de sa déclaration de struct, en annonçant une erreur d'appel. Deux conséquences pour qui écrit
un snippet : le nom de la classe Python (CatnipTypeError) et celui du runtime (TypeError) sont acceptés
indifféremment, et une erreur que le moteur VM ne nomme pas — il enveloppe les échecs sémantiques dans un RuntimeError
nu — est confirmée par le pipeline Python, qui la nomme. Ce repli ne rattrape qu'un défaut de rapport : si le moteur a
nommé un type, c'est celui-là qui compte, sans seconde chance auprès d'un autre moteur.
Un snippet qui appelle sys.exit() se traite comme un programme qui sort : statut zéro, le bloc réussit ; statut non
nul, c'est une erreur, donc expect-error. Rien à exclure. Le vérificateur évaluant dans son propre processus, ce cas
l'a longtemps terminé au lieu de le faire échouer — SystemExit dérive de BaseException, que la clause de garde
ne rattrapait pas : code de retour 1, aucune ligne d'échec, décompte final jamais imprimé. La garde est dans
eval_code, que les deux vérificateurs partagent.
check-nd-parity couvre un axe que les tests ordinaires ne voient pas : Catnip a trois exécuteurs (PureVM, VM PyO3,
AST) et une opération de broadcast qui ne se comporte pas pareil dans les trois est un défaut, quel que soit celui qui a
raison. La suite Python tourne en VM puis en AST, mais jamais dans le PureVM ; personne ne comparait les trois. Deux
détails font la validité du contrôle : les expressions viennent d'une source unique envoyée telle quelle aux deux
harnais (les générer de chaque côté donnerait un accord qui ne prouve rien), et la comparaison est sémantique — deux
exécuteurs qui lèvent tous les deux sont d'accord même si le message diffère, puisque le PureVM et CPython ne rédigent
pas pareil. Ce qui est signalé, c'est « l'un lève et l'autre rend une valeur », ou deux valeurs différentes.
Les divergences déjà tracées sont répertoriées dans le script (dictionnaire KNOWN) et n'échouent pas ; en retirer une
réactive la garde, ce qui est aussi la façon de vérifier que le contrôle mord encore. KNOWN est vide depuis le
2026-07-25 : les trois exécuteurs s'accordent sur les 97 cas, donc toute divergence qui apparaît fait échouer la cible.
Y remettre une entrée est une décision, pas un contournement — elle demande une ligne qui dit laquelle et pourquoi.
Un accord des trois ne suffit pas à conclure qu'un cas est correct : ils peuvent converger vers la même erreur (les
trois itéraient un bytes là où la spécification en fait un scalaire, et se déclaraient donc d'accord). Le contrôle
répond « ils font la même chose », la spécification répond « c'est la bonne ».
make test-all lance lint-rust en premier (avant les tests) : un warning Clippy arrête la suite tout de suite, sans
attendre les tests Python. Les crates PyO3 (catnip_rs, catnip_repl) sont lintés avec --features embedded ;
extension-module, le feature par défaut, ne se linke pas hors de Python.
Structure du projet
catnip/ # Module Python (API, CLI, context)
catnip_rs/ # Extension Rust principale (PyO3)
src/
parser/ # Transformateurs Tree-sitter → IR
semantic/ # Analyseur sémantique + passes d'optimisation
core/registry/ # Dispatch des opérations
vm/ # VM bytecode (NaN-boxing, JIT)
ir/ # Opcodes IR (source de vérité)
cfg/ # CFG + SSA
jit/ # JIT trace-based (Cranelift)
catnip_grammar/ # Grammaire Tree-sitter (grammar.js)
catnip_repl/ # REPL interactive (ratatui)
catnip_tools/ # Formatter + Linter
catnip_lsp/ # Serveur LSP (diagnostics, formatting, rename)
catnip_mcp/ # Serveur MCP utilisateur
tests/ # Tests Python d'intégration
language/ # Tests comportement langage (parité VM/AST)
optimization/ # Tests passes d'optimisation
serial/ # Tests JIT (non parallélisables)
Problèmes courants
make compile échoue avec "can't find crate"
Le workspace Cargo a plusieurs crates. Vérifier que Cargo.lock est à jour :
cargo update
make compile
Import error catnip._rs
L'extension Rust n'est pas compilée ou pas à la bonne place :
make compile
# Si ça persiste :
make reinstall-lang
Tests qui échouent après modification d'opcodes
Les opcodes Python sont générés depuis Rust. Si les deux sont désynchronisés :
make check-opcodes # Diagnostique le problème
make gen-opcodes # Régénère les fichiers Python
make compile # Recompile (gen-opcodes est aussi appelé automatiquement)