Spinel - Compilateur natif AOT pour Ruby
(github.com/matz)- Un compilateur AOT qui effectue une inférence de types à l’échelle du programme entier sur le code source Ruby, puis le convertit en code C afin de générer un binaire natif autonome
- Développé directement par matz, le créateur de Ruby ; le backend du compilateur lui-même est écrit en Ruby dans une architecture auto-hébergée capable de se compiler lui-même
- Des performances environ 11,6 fois supérieures à miniruby (Ruby 4.1.0dev) ; Conway's Game of Life est 86,7 fois plus rapide, ackermann 74,8 fois, et mandelbrot 58,1 fois
- Le pipeline de compilation transforme d’abord Ruby en texte AST via un parseur basé sur Prism, puis le backend auto-hébergé réalise l’inférence de types et la génération de code C, avant qu’un compilateur C standard ne produise un binaire standalone
- Prise en charge d’un large éventail de fonctionnalités Ruby : classes, héritage, blocs, gestion des exceptions, Fiber, moteur Regexp NFA intégré, Bigint à promotion automatique, pattern matching, etc.
- Les petites classes avec 8 champs scalaires ou moins sont automatiquement allouées sur la pile comme value types, éliminant totalement la surcharge du GC (1 million d’allocations : 85 ms → 2 ms)
- Les concaténations de chaînes
a + b + c + dsont aplaties en un seul malloc, etsplitdans une boucle réutilisesp_StrArraypour supprimer les allocations inutiles - Nombreuses optimisations à la compilation : remontée hors boucle des longueurs invariantes, propagation de constantes, inlining automatique des méthodes de 3 instructions ou moins, arrêt anticipé de l’inférence répétée (~14 % de réduction du temps de bootstrap), etc.
- Les binaires générés ont zéro dépendance d’exécution et peuvent fonctionner avec seulement libc + libm
eval, la métaprogrammation, ainsi que Thread et Mutex ne sont pas pris en charge (seul Fiber est supporté)- Licence MIT
1 commentaires
Avis sur Hacker News
Si c’est Matz qui l’a fait, il connaît sûrement très bien les limites de la sémantique Ruby, donc ça inspire confiance
Mon mémoire de master portait aussi sur un compilateur JS AOT ; ça fonctionnait, mais les contraintes sur les données d’entrée étaient trop fortes, donc j’ai fini par abandonner
À l’époque, les développeurs JS n’étaient pas vraiment habitués à s’imposer ce genre de contraintes, et des entrées fondamentalement indéterminables comme
JSON.parseposaient problèmeAujourd’hui, avec TypeScript, ce serait peut-être bien plus réaliste qu’à l’époque
Rien qu’avec le lambda calculus classique, les limites de l’inférence de types sont évidentes, et on retrouve des contraintes similaires dans les papiers de Matt Might ou dans le travail sur Shed-skin Python
Je me demande à quel point
eval,send,method_missingetdefine_methodsont fréquents dans du vrai code Ruby, et aussi comment on gère en général du parsing non typé, par exemple des entrées JSONLe parsing Ruby est presque plus difficile que la traduction elle-même, donc ils utilisent Prism, puis génèrent du C
La sémantique Ruby de base n’est pas si difficile à implémenter
À l’inverse, moi je m’accroche à un vieux compilateur AOT auto-hébergé écrit en Ruby pur, et comme j’ai insisté pour utiliser mon propre parseur, je me suis volontairement compliqué la vie
J’ai appris assez tôt que les premiers 80 % peuvent être bricolés et permettent déjà d’exécuter une grosse partie du code Ruby, et que le vrai « deuxième 80 % » se concentre sur tout ce que Matz a laissé de côté ici et dans mruby, comme les encodings et toutes sortes de fonctionnalités périphériques
Honnêtement, Ruby a aussi pas mal de fonctionnalités que je n’ai jamais vues une seule fois dans du vrai code, donc ça ne me choquerait pas que certaines soient dépréciées
send,method_missingetdefine_methodsont très courantsLes contraintes ressemblent à celles de mruby, et il y a quand même des usages possibles dans ce cadre
Le support de
send,method_missingetdefine_methodest relativement facileEn revanche, le support de eval() est extrêmement pénible
Cela dit, dans Ruby, une grande partie des usages de
eval()peut être ramenée statiquement à la version bloc de instance_eval, et dans ces cas-là, la compilation AOT devient assez simplePar exemple, s’il est possible de connaître statiquement ou de décomposer la chaîne passée à
eval(), il y a beaucoup plus de marge de manœuvreEn pratique, beaucoup d’usages de
eval()sont inutiles ou servent juste à contourner simplement l’introspection, donc une analyse statique peut les traiterDans mon compilateur aussi, si ça devient le goulot d’étranglement, c’est par là que je commencerai
L’ingestion de JSON non typé utilisera probablement aussi ce type de mécanisme
Si on enlève tout ça, il reste un langage petit et lisible, moins fortement typé que Crystal mais qui dépend aussi moins de la métaprogrammation que Ruby officiel
Donc le potentiel a l’air réel, mais au final seul le temps le dira
evalJe pourrais m’en passer, mais pour moi c’est plus ergonomic
eval,exec,define_method, ainsi que le pattern qui consiste à créer de nouvelles classes avecClass.newetStruct.newLa plupart de ces usages se concentrent au démarrage de l’application ou pendant le chargement de fichiers via require, ce qui ressemble déjà, d’une certaine façon, à une étape de compilation
C’est ce que Matz vient tout juste de présenter à RubyKaigi 2026
C’est expérimental, mais ça a été construit en environ un mois avec l’aide de Claude, et la démo en direct a fonctionné
Le nom vient du nouveau chat de Matz, lui-même nommé d’après le chat de Card Captor Sakura, qui forme à son tour une paire avec un personnage nommé Ruby
Et pour quelqu’un comme Matz, ça pourrait même faire passer de 100x à 500x
https://en.wikipedia.org/wiki/Spinel
La vidéo n’a pas l’air encore en ligne, on dirait qu’elles sont publiées une par une sur cette chaîne
https://www.youtube.com/@rubykaigi4884/videos
Le nom du projet donne aussi l’impression d’avoir été choisi de manière émotionnelle
C’est évidemment très impressionnant, mais sans agent IA, ça a l’air impossible à maintenir
spinel_codegen.rbfait 21 000 lignes, et certaines méthodes sont imbriquées jusqu’à 15 niveauxLe code de compilateur est rarement joli au départ, mais même selon ce standard, celui-ci semble extrêmement difficile à maintenir humainement
Les frontières entre sous-systèmes y sont nettes et les handoffs entre phases bien définis, donc c’est même l’un des domaines les plus faciles à rendre modulaires
Le problème, en général, c’est qu’on trouve le temps de le faire marcher, mais pas de le refactorer ensuite, et la saleté s’accumule
spinel_codegen.rbest quasiment au niveau horreur indicibleAvec Claude, je finis toujours avec ce genre de spaghetti code, donc je me demandais ce que je faisais mal
Mais en voyant qu’un projet vraiment intéressant, écrit par quelqu’un que je considère comme un programmeur de tout premier plan, contient lui aussi du code d’assez mauvaise qualité par endroits, je me dis que ça ne m’arrive pas qu’à moi
Par exemple,
infer_comparison_type()n’est pas le pire cas et ce n’est pas illisible, mais il existe une implémentation bien plus simple et claire, et pourtant Claude n’y arrive pasRegrouper les opérateurs de comparaison dans un
Setpuis utiliserinclude?serait plus court, plus rapide, plus lisible et plus facile à maintenirMais Claude dérive toujours vers des enchaînements de if-return, et on a presque l’impression que même le if-else lui est étranger
Mon propre code généré avec Claude est lui aussi plein de ce genre de patterns, donc ça me rassure de voir que je ne suis pas le seul
En revanche, les autres fichiers sont bien meilleurs, surtout le répertoire
lib, qui semble correspondre au répertoireextdu dépôt Ruby principal et dont la qualité est correcteL’API porte clairement l’influence de MRI Ruby et, même si l’implémentation diffère beaucoup, Matz semble avoir orienté Claude pour qu’elle ressemble partiellement à l’API d’origine, ce qui rend le résultat plus cohérent
[1] https://github.com/matz/spinel/blob/98d1179670e4d6486bbd1547...
Si les tests et les benchmarks passent, ça me suffit pour l’instant
En revanche, je me demande si un fichier gigantesque est facile à manipuler même pour une IA
J’essaie de limiter mes fichiers à moins de 300 lignes, et je pense qu’un code facile à comprendre pour un humain le sera aussi pour les agents de code
D’après ce qui a été dit, les contraintes sont les suivantes
No eval :
eval,instance_eval,class_evalNo metaprogramming :
send,method_missing,define_method(dynamique)No threads :
Thread,Mutex(Fiber est pris en charge)No encoding : hypothèse UTF-8/ASCII
No general lambda calculus :
-> x { }profondément imbriqué avec appels[]L’hypothèse UTF-8/ASCII n’est pas une grosse contrainte pour moi, mais le reste risque d’être une vraie limitation pour beaucoup de programmes
Et les réintroduire demanderait visiblement un travail conséquent
J’utilise Ruby depuis longtemps, et après avoir utilisé toutes les fonctionnalités listées, j’ai plutôt l’impression qu’au terme de cette évolution, c’est justement cette version de Ruby simplifié que j’ai fini par vouloir
C’est plus simple, plus facile à comprendre, tout en conservant l’esthétique propre à Ruby
Avec les LLM, la productivité en génération de code est maintenant si élevée qu’il est moins nécessaire qu’avant de réduire le boilerplate via la métaprogrammation pour améliorer la productivité des développeurs
Parce que la part du code réellement écrite à la main par les développeurs diminue elle-même
La syntaxe est proche et il y a un système de types statique, ce qui conduit à un code compilé plus efficace
evalne me gêne pas vraiment, mais ne pas avoir threads et mutexes, c’est dommageL’absence de
define_methodse comprend vu ses usagesEn revanche,
sendetmethod_missingsont fréquents dans les bibliothèques existantes, et leur implémentation ne semble pas si difficile si on construit à la compilation une table de lookup en mémoireDonc je ne sais pas si c’est un choix délibéré ou si ce n’est simplement pas encore fait
J’espère que c’est le second cas, mais au moins à court terme, la compatibilité empêchera sans doute un usage en production
C’était de réduire la quantité de code à lire
C’est vraiment formidable, et j’attends depuis longtemps un compilateur AOT pour Ruby
Cela dit, c’est dommage qu’il n’y ait pas de fallback pour
evalou la métaprogrammation, même si ça semble découler d’un choix de se concentrer sur un petit sous-ensemble très performantJ’aimerais que les gems produites avec ce compilateur AOT interagissent bien avec MRI et Ruby
Pour empaqueter ou regrouper du Ruby standard et des gems, il faut toujours des outils comme tebako, kompo, ocran, et auparavant il y avait aussi des projets comme ruby-packer, traveling ruby ou jruby warbler
C’est bien d’avoir une option de plus, mais j’espère toujours une solution définitive avec une meilleure UX développeur
Ça faisait trop longtemps qu’il n’avait pas été mis à jour
Je me demande pourquoi pas de threads
Le scheduler Ruby et l’implémentation pthread en dessous semblent pouvoir fonctionner aussi en C, donc je me demande s’il s’agit d’un choix visant le zéro dépendance
S’il n’est ni prévu comme extension optionnelle plus tard, ni simplement pas encore implémenté, ce choix me paraît un peu étrange
À mon avis, ils n’en sont simplement pas encore là
Le multithreading est de toute façon notoirement très difficile à bien faire
C’est surprenant que ça ait été fait en un peu plus d’un mois
On peut dire ce qu’on veut sur l’IA, mais entre les mains d’un développeur compétent, elle apporte un gain de vitesse énorme
Matz, lui, donne l’impression que
gem env|infoetfindlui suffisent largementVu que c’est Matz qui l’a fait, je me demande à quel point il est réaliste que ça devienne un jour une partie du Ruby core
Et si c’était le cas, je me demande aussi à quel point ce serait une menace pour Crystal
Ces caractéristiques sont pratiquement indispensables pour compiler et maintenir de gros programmes
Ici, au contraire, on parle d’un sous-ensemble restreint de Ruby, donc la plupart des gems Ruby populaires ne fonctionneront probablement pas telles quelles
En tant que sous-ensemble de langage visant la compilation en C, ça ressemble davantage à PreScheme
À ce stade, je ne vois pas les deux en concurrence directe sur le même terrain
Le Ruby complet aura presque certainement besoin d’un JIT
[1]: https://prescheme.org/
Ce sera une sorte de revanche du Rational Unified Process et d’Enterprise Architect
La différence, c’est qu’au lieu de diagrammes UML, on recevra des fichiers markdown
Ça semble utile du côté des outils d’infrastructure
On peut imaginer, par exemple, un bundler écrit en Ruby mais compilé statiquement, qui ferait aussi office d’outil d’installation Ruby à la manière de RVM
Les buildpacks Ruby existants sont écrits en Ruby, mais il faut les amorcer avec bash, ce qui est pénible et crée des cas limites
CNB a été écrit en Rust pour éviter ce problème, et l’idée de pouvoir distribuer un binaire unique sans dépendance est vraiment puissante