v12
Un moteur JavaScript intégrable écrit en Rust pur de zéro : sans V8, sans QuickJS, sans compromis de vitesse.
Ce que c'est
Les options pour exécuter du JavaScript en Rust sont souvent des bindings C++ lourds vers V8 ou des interpréteurs AST lents. Je voulais concevoir un moteur natif en Rust pensé pour la vitesse dès le départ : bytecode à registres, NaN-boxing, classes cachées et compilateur JIT.
L'objectif : un moteur intégrable qui démarre instantanément, consomme peu de mémoire et exécute du vrai code sans embarquer des centaines de mégaoctets de C++.
Architecture & pipeline
Le projet sépare l'analyse, l'exécution bytecode, la mémoire et la compilation JIT en crates ciblées :
v12-bytecode&v12-bccompiler: Compile l'AST oxc en instructions 32 bits à registres avec tables de constantes et d'exceptions.v12-heap: Représentation NaN-boxée sur 64 bits, classes cachées avec arbres de transition et allocateur par arènes.v12-interp: Interpréteur Tier-0 avec suspension des générateurs et profilage JIT.v12-jit-baseline&v12-codegen: Compilateur JIT Tier-1 basé sur Cranelift pour compiler les fonctions chaudes en code machine.v12-api: Façade d'intégration exposant unContextsimple pour exécuter des scripts et lier des fonctions Rust.
flowchart LR
JS["JavaScript Source"] --> PARSER["oxc Parser & AST"]
PARSER --> COMPILER["v12-bccompiler"]
COMPILER --> BC["v12-bytecode (Register ISA)"]
BC --> INTERP["Tier-0 Interpreter"]
BC --> JIT["Tier-1 Cranelift Template JIT"]
INTERP <--> HEAP["Handle-based Heap & Mark-Sweep GC"]
JIT <--> HEAP
HEAP --> SHAPES["Hidden Classes & Inline Caches"] Les classes cachées et les inline caches ressemblent à de la magie noire jusqu'à ce qu'on écrive soi-même l'arbre de transition.
Décisions clés
NaN-boxing. Toutes les valeurs JS (nombres, booléens, null, undefined, handles d'objets) tiennent dans un mot de 64 bits. Les types primitifs ne touchent jamais la mémoire heap.
Classes cachées & inline caches. Suivre la structure des objets dynamiquement permet de cacher les offsets de propriétés, transformant des lookups de hash map en simples additions de pointeurs.
Heap à handles & GC mark-sweep. Les objets vivent dans des arènes indexées par des indices de 32 bits. Cela évite de scanner la pile native Rust et garantit la stabilité des références.
Les parties difficiles
Async/await et générateurs sans dérouler la pile. Quand le code attend une promesse ou fait un yield, il faut geler la fenêtre de registres, sauvegarder la portée et reprendre plus tard sans bloquer le thread.
La suite Test262. JavaScript regorge de cas limites obscurs. Valider plus de 7 500 tests Test262 a signifié déboguer des coercitions de types, des zones mortes temporelles et des chaînes de prototypes tard dans la nuit.
Ce que j'ai appris
Concevoir un moteur JS mélange compilateurs, gestion fine de la mémoire et respect des spécifications. Passer les tests Test262 m'a appris à quel point la conformité et la performance tirent constamment dans des directions opposées.