0.1.1 (2026-06-13)
Corrections issues d'une revue de code et de l'alignement du mode AST sur la VM (correctness et sûreté).
Méthodes sur les unions taggées : une union peut déclarer des méthodes après ses variantes -- au niveau de
l'union, jamais par variante. self reçoit la variante appelée et le corps discrimine par match ; le linter vérifie
l'exhaustivité de ces match avec les variantes manquantes précises (self est typé comme l'union déclarante). Pas
d'init (les constructeurs sont les variantes), pas d'@abstract (pas d'héritage d'union), pas de décorateurs ni
d'opérateurs surchargés : une seule forme de dispatch pour toutes les variantes. Câblé sur les trois exécuteurs (VM,
AST, runtime pur). Voir docs/lang/UNIONS.md §Méthodes.
Unions dans le runtime pur : le runtime sans Python (serveur MCP, PurePipeline) exécute désormais les unions --
déclaration, construction, pattern matching, méthodes, export par module -- au lieu de rejeter MakeUnion. L'égalité du
runtime pur devient structurelle pour les opcodes ==/!= et pour in/not in sur listes et tuples (structs, listes,
dicts, sets ; elle comparait par identité), et print/str()/repr() ainsi que les f-strings affichent correctement
structs et variantes (P(x=1, y=2), Color.red au lieu d'identifiants opaques).
Repr des variantes en résultat : catnip -c 'enum D { up }\nD.up' affiche D.up au lieu de l'index interne du
symbole (la conversion du résultat se faisait après désinstallation des tables du VM).
Proper tail calls : tout appel par nom en position terminale s'exécute en pile constante, plus seulement
l'auto-récursion -- récursion mutuelle (is_even/is_odd à un million d'appels : ~30 MB au lieu de ~800 MB), fonctions
imbriquées récursives, appels terminaux vers une autre fonction, et la forme match (jamais optimisée auparavant), dans
les deux modes d'exécution. En mode AST les appels terminaux n'étaient en réalité jamais optimisés (plafond vers ~10 000
appels) : un million d'appels passe désormais. Les écritures aux variables capturées survivent aux changements de
fonction dans une chaîne terminale, et les défauts de paramètres sont liés à chaque appel. Contrepartie assumée : les
cadres intermédiaires d'une chaîne terminale n'apparaissent plus dans les stack traces -- pragma("tco", False) les
restaure pour debugger.
Groupes de fonctions récursives : une fonction nommée définie dans une autre fonction peut s'appeler elle-même en
mode VM (levait NameError ; le mode AST fonctionnait déjà), et even/odd définies dans le même bloc se voient entre
elles, même hors de leur scope de définition. Les liaisons sont figées à la définition : réassigner le nom ou créer un
alias ne change pas ce que les fonctions s'appellent -- même comportement dans les deux modes. Pickle : la
self-référence survit ; sérialiser une fonction d'un groupe mutuel ne conserve que sa self-référence (avant : erreur de
récursion infinie).
Retrait d'API : catnip.semantic ne réexporte plus l'ancien pipeline d'optimisation (Semantic, Optimizer et les
classes *Pass), qui ne participait plus à l'exécution depuis la migration du pipeline en Rust. OpCode et les
ensembles d'opcodes (CONTROL_FLOW_OPS, etc.) restent disponibles. L'analyse sémantique passe exclusivement par
Catnip.parse().
- Résultats corrects avec l'optimiseur : certaines simplifications du compilateur changeaient des valeurs
observables --
"abc" * 0rendait0au lieu de"",7.5 // 1rendait7.5au lieu de7.0,5 == Truerendait5au lieu deFalse, comparaisons faussées en présence deNaN, etmatch expr { _ => ... }n'évaluait pasexpr(effets perdus). Le code optimisé se comporte désormais comme le code non optimisé. - Messages d'erreur du mode AST : les noms indéfinis produisaient un message doublement enveloppé
(
Name "name 'x' is not defined" is not defined). Le message est désormais identique au mode VM. - Pragmas
optimizeettcohonorés dans leur propre fichier :pragma("optimize", 0)etpragma("tco", False)n'avaient aucun effet sur le fichier qui les contenait (lus trop tard). Ils s'appliquent désormais dès la compilation, partout (CLI, API Python, binaire standalone, MCP, LSP). Les options de la ligne de commande (-o level:N,-o tco:on/off) gardent le dernier mot sur les pragmas du fichier. list.pop(i)en mode standalone :catnip-run, le serveur MCP et le LSP retiraient toujours le dernier élément quel que soit l'index passé.pop(0)rend désormais le premier élément, comme attendu (le mode Python était déjà correct)."x" * ndémesuré : une répétition de chaîne trop grande renvoie une erreur récupérable au lieu d'interrompre le processus.- Cache disque plus fiable : l'expiration (TTL) ne dépend plus de l'horodatage du système de fichiers, l'éviction LRU reflète réellement les derniers accès, et un fichier de cache corrompu est ignoré puis purgé au lieu de faire échouer la lecture.
- Boucles à exceptions : plus de fuite mémoire quand une boucle lève et rattrape des exceptions de façon répétée.
- JIT : résultats corrects dans des cas limites (trace rechargée depuis le cache disque, plusieurs fonctions récursives mémoïsées dans le même processus).
- Débogueur : fermeture de session plus propre -- plus de thread d'exécution abandonné, et quitter ne tue plus le host process.
- Formatter sur les struct patterns à variant : dans un
match, un motif commeState.Ready{data}était reformaté enState{Ready, data}-- le qualificateur de variant.Readyétait perdu et compté comme un champ destructuré. Le nom de variant est désormais conservé (catnip formatet l'outil MCPformat_code). - Enums en mode AST : définir un enum échouait systématiquement (erreur interne), et
typeofsur une variante retournait le nom du type interne ('catnipenumvariant') au lieu du nom de l'enum. Les deux suivent désormais le comportement VM, y compris pour les variantes importées d'un module. - JIT en mode AST : activer le JIT (
-o jit) en mode AST levait une erreur interne. Le JIT reste propre à la VM ; l'option est désormais ignorée sans erreur. - Paramètres vs captures en mode AST : un paramètre de fonction homonyme d'une variable capturée écrasait la variable du scope parent après l'appel.
- Pickle de lambdas récursifs en mode AST : un lambda récursif nommé perdait sa self-référence à la désérialisation (la VM la restaurait déjà).
- Positions d'erreur en mode AST : une erreur dans un corps de fonction ou un bloc imbriqué était rapportée à la ligne du statement top-level englobant. Ligne et colonne pointent désormais le nœud fautif, comme en VM.
- Imports en mode AST : la résolution passait par un loader distinct de celui de la VM -- les plugins natifs
(
http) résolvaient vers le module Python homonyme,import('io', protocol='py')masquait le module natifiopour les imports suivants, et les méthodes d'objets plugin (r.json()) étaient inaccessibles. Le même loader (et son cache) sert désormais les deux modes d'exécution.