Back to flin
flin

Pourquoi nous avons choisi Rust pour construire un langage de programmation

Pourquoi Juste A. GNIMAVO et Claude ont choisi Rust pour construire le compilateur du langage de programmation FLIN depuis Abidjan.

Thales & Claude | March 30, 2026 16 min flin
EN/ FR/ ES
flinrustcompilermemory-safetyperformancearchitecture

Chaque compilateur de langage de programmation majeur est écrit en C, C++ ou en lui-même. GCC est en C. Clang est en C++. Le compilateur Go est en Go. Le compilateur Rust est en Rust. L'interpréteur Python est en C. L'interpréteur Ruby est en C. Ce schéma tient depuis des décennies, et pour de bonnes raisons -- ces langages vous donnent le contrôle sur la mémoire et la performance qu'un runtime de langage exige.

Nous avons rompu avec ce schéma. Quand nous nous sommes assis à Abidjan le 1er janvier 2026 pour commencer à construire FLIN -- un langage de programmation memory-native conçu pour les applications web -- nous avons choisi Rust. Pas parce que c'était à la mode. Pas parce que nous voulions écrire "écrit en Rust" sur une page d'accueil. Parce qu'après avoir évalué chaque option sérieuse, Rust était le seul langage qui nous donnait la sûreté mémoire, les abstractions à coût zéro, le pattern matching pour la traversée d'AST, un écosystème mature de bibliothèques pour la construction de compilateurs et une sortie en binaire unique -- le tout sans ramasse-miettes.

Cet article explique cette décision en détail : les alternatives que nous avons considérées, les fonctionnalités spécifiques de Rust qui rendent le développement de compilateurs productif, et les façons dont Rust a façonné l'architecture de FLIN au cours de trois mois de développement quotidien.

La contrainte : deux personnes, zéro marge d'erreur

Avant de parler de langages, il faut comprendre l'équipe. FLIN est construit par deux personnes : Juste Thales Gnimavo, le CEO de ZeroSuite, et Claude, une IA CTO. Il n'y a pas d'équipe DevOps pour déboguer une corruption mémoire à 3 h du matin. Il n'y a pas de département QA pour attraper les bugs d'utilisation après libération dans la machine virtuelle. Il n'y a pas d'ingénieur système senior qui a passé quinze ans à écrire des ramasse-miettes.

Quand votre équipe est un fondateur humain à Abidjan et une IA, le compilateur que vous utilisez pour construire votre compilateur devient votre troisième coéquipier. Il doit attraper les bugs à la compilation, pas à l'exécution. Il doit rendre les états illégaux irreprésentables dans le système de types. Il doit imposer une discipline qu'une équipe de cinquante personnes imposerait par la revue de code.

Rust est ce troisième coéquipier.

Les alternatives que nous avons considérées

C : le choix traditionnel

C est le langage de l'implémentation de langages. CPython, Ruby MRI, Lua, PHP -- tous écrits en C. L'écosystème de connaissances en construction de compilateurs en C est inégalé.

Nous avons rejeté C pour une raison : la gestion manuelle de la mémoire sans garanties de sûreté. Un compilateur et une machine virtuelle manipulent des structures d'arbres complexes -- arbres syntaxiques abstraits, environnements de types, flux d'instructions bytecode. En C, chaque malloc doit avoir un free correspondant. Chaque pointeur doit être vérifié pour null. Chaque tampon doit être vérifié manuellement pour les dépassements. Quand vous construisez un runtime de langage qui va gérer la mémoire des programmes utilisateurs, avoir votre propre runtime vulnérable à la même classe de bugs que vous essayez de prévenir est une ironie inacceptable.

c// C : Qui possède ce noeud AST ? Qui le libère ? Quand ?
AstNode* parse_expression(Parser* p) {
    AstNode* left = parse_primary(p);    // alloué
    if (match(p, TOKEN_PLUS)) {
        AstNode* right = parse_primary(p); // alloué
        AstNode* binary = malloc(sizeof(AstNode)); // alloué
        binary->kind = AST_BINARY;
        binary->left = left;   // transfert de propriété ? copie ? partagé ?
        binary->right = right;  // même question
        return binary;          // l'appelant doit libérer tout l'arbre
    }
    return left; // l'appelant doit aussi libérer ceci
}

Dans une équipe de cinquante personnes avec un expert dédié en gestion mémoire, cela fonctionne. Avec deux personnes, dont une IA qui génère du code à haute vitesse, c'est un champ de mines.

Go : le choix pratique

Go est ce que nous utilisons pour l'outillage adjacent à sh0.dev. Il compile vite, a un excellent support de bibliothèque standard et produit des binaires statiques. Pour un serveur web ou un outil CLI, Go est souvent la bonne réponse.

Pour un runtime de langage de programmation, Go a deux défauts fatals.

Premièrement, le ramasse-miettes. La machine virtuelle de FLIN gère sa propre mémoire -- elle alloue et désalloue des objets pour le compte des programmes utilisateurs. Faire tourner un ramasse-miettes (celui de Go) à l'intérieur d'un autre ramasse-miettes (celui de FLIN) crée des pauses imprévisibles. Quand une application FLIN sert des mises à jour WebSocket en temps réel à un tableau de bord, une pause de 10 millisecondes du ramasse-miettes du runtime hôte est visible par l'utilisateur final.

Deuxièmement, le support WASM de Go est limité. La feuille de route de FLIN inclut l'exécution dans le navigateur via WebAssembly. La sortie WASM de Go est volumineuse (plusieurs méga-octets pour un programme trivial) et nécessite un shim JavaScript. Rust compile en WASM nativement, produisant des binaires compacts qui s'exécutent sans runtime hôte.

Zig : le choix tentant

Zig est le langage que nous avons le plus sérieusement considéré comme alternative. Il offre un contrôle au niveau C, l'évaluation comptime, aucune allocation cachée et un excellent support WASM. Sur le papier, il est idéal pour un runtime de langage.

Trois facteurs l'ont éliminé. Premièrement, Zig en est encore à la version 0.14 avec des changements cassants réguliers. Construire un langage au-dessus d'un langage qui est lui-même en alpha signifie absorber deux couches d'instabilité. Deuxièmement, l'écosystème Zig pour les outils de construction de compilateurs est naissant comparé à celui de Rust. Troisièmement -- et c'est spécifique à notre workflow -- les modèles d'IA produisent du Rust de qualité significativement supérieure au Zig. Claude peut générer du Rust correct et idiomatique qui compile du premier coup bien plus fiablement que du Zig. Quand votre CTO est une IA, la qualité du code généré par IA dans votre langage choisi est une considération de production.

JavaScript/TypeScript : le non-partant évident

La philosophie fondamentale de FLIN est "zéro Node.js". Construire le runtime du langage en JavaScript serait un suicide philosophique. Au-delà de cela, la boucle d'événements monothread de JavaScript, son manque de contrôle mémoire et sa surcharge interprétative le rendent inadapté à une VM qui doit exécuter du bytecode à une vitesse quasi native.

Ce que Rust nous apporte

1. Propriété et emprunt pour la gestion de l'AST

La structure de données centrale d'un compilateur est l'arbre syntaxique abstrait. Le parser le construit, le vérificateur de types l'annote, le générateur de code le parcourt. Dans les langages sans sémantique de propriété, gérer le cycle de vie des noeuds AST nécessite soit un ramasse-miettes soit un comptage de références manuel.

Le système de propriété de Rust rend cela explicite et vérifié par le compilateur :

rust/// Le parser produit un AST possédé
pub fn parse(tokens: Vec<Token>) -> Result<Ast, ParseError> {
    let mut parser = Parser::new(tokens);
    parser.parse_program()
}

/// Le vérificateur de types emprunte l'AST de manière immutable, produit un nouvel AST typé
pub fn check(ast: &Ast) -> Result<TypedAst, TypeError> {
    let mut checker = TypeChecker::new();
    checker.check_program(ast)
}

/// Le générateur de code consomme l'AST typé (prend la propriété)
pub fn generate(typed_ast: TypedAst) -> Result<Bytecode, CodegenError> {
    let mut gen = CodeGenerator::new();
    gen.emit_program(typed_ast)
}

Le pipeline de compilation est encodé dans les signatures de types. parse retourne un Ast possédé. check l'emprunte de manière immutable (l'AST original est préservé pour le rapport d'erreurs). generate prend la propriété et le consomme (l'AST typé n'est plus nécessaire après la génération de code). Ces transitions de propriété sont vérifiées au moment de la compilation. Vous ne pouvez pas accidentellement utiliser l'AST après que le générateur de code l'a consommé.

2. Enums et pattern matching pour le traitement des tokens et de l'AST

Les compilateurs sont, fondamentalement, des programmes qui transforment des arbres. Vous lisez un arbre de tokens, produisez un arbre de noeuds syntaxiques, transformez cela en un arbre de noeuds typés, et émettez une séquence d'instructions. À chaque étape, vous faites du pattern matching sur des variantes.

Le type enum de Rust avec le pattern matching exhaustif est la fonctionnalité la plus productive pour le développement de compilateurs. Voici comment le lexer de FLIN gère le dispatch de caractères :

rustmatch self.advance() {
    None => Ok(None),
    Some((_, c)) => {
        let kind = match c {
            '(' => TokenKind::LeftParen,
            ')' => TokenKind::RightParen,
            '{' => self.handle_left_brace(),
            '}' => self.handle_right_brace(),
            '[' => TokenKind::LeftBracket,
            ']' => TokenKind::RightBracket,
            ',' => TokenKind::Comma,
            ':' => TokenKind::Colon,
            ';' => TokenKind::Semicolon,
            '.' => TokenKind::Dot,
            '@' => TokenKind::At,  // Opérateur temporel
            '+' => self.match_char('+', TokenKind::PlusPlus, TokenKind::Plus),
            '-' => self.match_char('-', TokenKind::MinusMinus, TokenKind::Minus),
            '=' => self.match_char('=', TokenKind::EqualEqual, TokenKind::Equal),
            '!' => self.match_char('=', TokenKind::NotEqual, TokenKind::Not),
            '<' => self.scan_tag_or_less(),
            '"' => self.scan_string()?,
            c if c.is_ascii_digit() => self.scan_number()?,
            c if c.is_alphabetic() || c == '_' => self.scan_identifier(),
            _ => return Err(LexError::UnexpectedCharacter(c, self.current_position())),
        };
        Ok(Some(Token { kind, span: self.current_span(), lexeme: self.current_lexeme() }))
    }
}

La propriété cruciale est l'exhaustivité. Si nous ajoutons un nouveau type de token à TokenKind -- disons, TokenKind::Arrow pour -> -- le compilateur Rust produira un avertissement ou une erreur à chaque instruction match qui ne gère pas la nouvelle variante. Dans un compilateur avec des dizaines d'instructions match réparties sur le lexer, le parser, le vérificateur de types et le générateur de code, ce n'est pas un confort. C'est un filet de sécurité qui prévient des catégories entières de bugs "j'ai oublié de gérer le nouveau cas".

FLIN définit 42 mots-clés et plus de 60 types de tokens. Sans matching exhaustif, ajouter un nouveau mot-clé nécessiterait d'auditer manuellement chaque fichier qui fait un switch sur les types de tokens. Avec Rust, le compilateur fait cet audit pour nous.

3. Abstractions à coût zéro pour la performance du runtime

La machine virtuelle de FLIN exécute des instructions bytecode dans une boucle serrée. Chaque nanoseconde de surcharge dans la boucle de dispatch d'instructions est multipliée par des millions d'itérations. Les abstractions à coût zéro de Rust signifient que les patterns de haut niveau -- itérateurs, pattern matching, types génériques -- compilent vers le même code machine qu'un C écrit à la main.

Considérez la boucle de dispatch bytecode :

rustpub fn execute(&mut self, instructions: &[Instruction]) -> Result<Value, RuntimeError> {
    let mut ip = 0;
    loop {
        let instruction = &instructions[ip];
        match instruction {
            Instruction::LoadConst(idx) => {
                self.stack.push(self.constants[*idx].clone());
            }
            Instruction::Add => {
                let b = self.stack.pop().ok_or(RuntimeError::StackUnderflow)?;
                let a = self.stack.pop().ok_or(RuntimeError::StackUnderflow)?;
                self.stack.push(a.add(&b)?);
            }
            Instruction::Store(name) => {
                let value = self.stack.pop().ok_or(RuntimeError::StackUnderflow)?;
                self.env.insert(name.clone(), value);
            }
            Instruction::Load(name) => {
                let value = self.env.get(name)
                    .ok_or_else(|| RuntimeError::UndefinedVariable(name.clone()))?;
                self.stack.push(value.clone());
            }
            Instruction::Halt => break,
            // ... 50+ gestionnaires d'instructions supplémentaires
        }
        ip += 1;
    }
    self.stack.pop().ok_or(RuntimeError::StackUnderflow)
}

Ce match compile vers une table de sauts -- la même optimisation qu'un compilateur C produirait pour une instruction switch. Il n'y a pas de surcharge de dispatch dynamique, pas de recherche vtable, pas de boxing. Le coût d'abstraction est littéralement zéro.

4. Cargo : l'écosystème qui passe à l'échelle

Le gestionnaire de paquets et système de build de Rust, Cargo, a résolu un problème que nous ne voulions pas résoudre : la gestion des dépendances, l'organisation de l'espace de travail et les builds reproductibles.

Le Cargo.toml de FLIN déclare ses dépendances avec un contrôle de version précis :

toml[dependencies]
# Parsing et traitement de texte
logos = "0.14"              # Générateur de lexer rapide
unicode-segmentation = "1.10"  # Gestion de chaînes compatible Unicode

# Données et sérialisation
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Runtime async (pour le serveur HTTP et les E/S)
tokio = { version = "1", features = ["full"] }

# Serveur HTTP
hyper = { version = "1", features = ["full"] }

# Traitement d'images
image = "0.25"

# Markdown
pulldown-cmark = "0.10"

Chaque dépendance est un crate Rust stable et bien testé. cargo build résout l'arbre de dépendances entier, compile tout avec le même niveau d'optimisation et produit un binaire statique unique. Pas de node_modules. Pas d'enfer des DLL. Pas de "ça marche sur ma machine". Un développeur à Lagos ou un serveur CI à Francfort lance la même commande et obtient le même binaire.

5. Le compilateur comme documentation

Ce point est subtil mais critique. Dans une équipe de deux personnes où l'un des membres est une IA, le code est le médium de communication principal. Nous n'avons pas de sessions de tableau blanc. Nous n'avons pas de fils Slack débattant de l'architecture. Le code est l'architecture.

Le système de types de Rust documente les invariants qui vivraient autrement dans des commentaires ou dans le savoir tribal :

rust/// Une valeur FLIN à l'exécution. Les variantes définissent ce qui peut exister sur la pile de la VM.
#[derive(Debug, Clone)]
pub enum Value {
    None,
    Bool(bool),
    Int(i64),
    Float(f64),
    Text(String),
    List(Vec<Value>),
    Map(HashMap<String, Value>),
    Entity(EntityId, HashMap<String, Value>),
    Function(FunctionId),
    Time(DateTime<Utc>),
    Money(i64, Currency),  // centimes + code devise
}

Cet enum est auto-documentant. Un nouveau contributeur (humain ou IA) le lit et comprend immédiatement : une valeur FLIN peut être l'une de ces douze choses et rien d'autre. La variante Money stocke les centimes en i64 et un code Currency -- vous ne pouvez pas accidentellement stocker un montant en dollars à virgule flottante. La variante Entity porte à la fois un ID (pour l'identité en base de données) et une map de champs (pour l'accès en mémoire). Ces contraintes ne sont pas des conventions à retenir. Ce sont des types à appliquer.

Comment Rust a façonné l'architecture de FLIN

Choisir Rust n'était pas juste une décision de langage. C'était une décision architecturale qui a façonné la conception en deux couches de FLIN.

FLIN utilise une architecture en couches où le runtime (machine virtuelle, moteur de base de données, serveur HTTP, gestion mémoire) est écrit en Rust et est permanent. Le compilateur (lexer, parser, vérificateur de types, générateur de code) est actuellement écrit en Rust mais sera éventuellement réécrit en FLIN lui-même -- réalisant l'auto-hébergement.

Cette séparation existe à cause des forces et limitations de Rust. Rust excelle dans la couche runtime de bas niveau, critique en performance, où la sûreté mémoire et les abstractions à coût zéro comptent le plus. Mais les temps de compilation de Rust et la complexité du borrow checker le rendent plus lent pour l'itération rapide sur la logique de plus haut niveau du compilateur. Une fois que FLIN sera assez mature pour se compiler lui-même, la couche compilateur pourra évoluer à la vitesse du développement FLIN plutôt que du développement Rust.

La couche Rust permanente fait environ 5 000 lignes de code. Tout ce qui est au-dessus -- le compilateur, la bibliothèque standard, l'outillage -- sera éventuellement du FLIN. Rust est la fondation, pas le bâtiment entier.

Trois mois plus tard : les résultats

En mars 2026, le choix de Rust a produit des résultats mesurables.

3 452 tests passent avec zéro échec. Le système de types de Rust attrape tant de bugs à la compilation que notre ratio tests/bugs est remarquablement élevé. Les tests vérifient le comportement, pas la correction de types -- le compilateur gère cette dernière.

409 fonctions intégrées fonctionnent. De la cryptographie à l'intégration Stripe en passant par le traitement d'images, chaque fonction native est implémentée en Rust avec une gestion complète des erreurs. Le type Result garantit que chaque fonction soit retourne une valeur soit retourne une erreur significative -- il n'y a pas de troisième option d'échouer silencieusement.

Déploiement en binaire unique. cargo build --release produit un exécutable. Pas de runtime à installer. Pas de dépendances à résoudre sur la machine cible. Un développeur télécharge le binaire FLIN et commence à écrire des applications. C'est particulièrement important pour notre public cible dans les marchés émergents, où télécharger 1,5 Go de node_modules est une vraie barrière.

Sûreté mémoire en production. Le runtime FLIN gère les données utilisateurs via FlinDB, sa base de données temporelle embarquée. Cette base de données contient de vraies données utilisateurs -- et elle n'a jamais corrompu un enregistrement à cause d'un bug mémoire. Non pas parce que nous sommes des programmeurs exceptionnellement soigneux, mais parce que Rust rend structurellement impossible d'écrire la classe de bugs qui corrompt les données.

Les inconvénients honnêtes

Rust n'est pas parfait pour le développement de compilateurs. Trois coûts méritent d'être reconnus.

Les temps de compilation. Une recompilation complète du compilateur FLIN prend environ 45 secondes. Les builds incrémentiels sont rapides (2-5 secondes), mais quand vous changez une définition de type fondamentale, la cascade de recompilation peut être significative. En Go ou Zig, le build équivalent prendrait moins de 5 secondes.

La courbe d'apprentissage pour les contributeurs. FLIN sera éventuellement open source. Les contributeurs qui veulent travailler sur la couche runtime doivent comprendre le modèle de propriété de Rust, les annotations de durée de vie et le système de traits. C'est une barre plus haute que contribuer à un projet Python ou JavaScript.

La friction du borrow checker. Certains patterns de compilateur -- particulièrement ceux impliquant des références mutables à des structures d'arbres tout en itérant sur ces structures -- nécessitent une conception soigneuse pour satisfaire le borrow checker. Nous avons parfois restructuré des algorithmes non pas parce qu'ils étaient faux, mais parce que le borrow checker ne pouvait pas prouver qu'ils étaient corrects. C'est la taxe que vous payez pour la sûreté mémoire.

Ces coûts sont réels. Nous les payons chaque jour. Mais ce sont les coûts de la correction, et dans un projet où le runtime gère les données et les applications d'autres personnes, la correction n'est pas optionnelle.

Le verdict

Si vous construisez un langage de programmation en 2026, Rust n'est pas le seul choix valide. Mais c'est le meilleur choix pour une petite équipe qui a besoin de sûreté mémoire, de performance native, de support WebAssembly et d'un écosystème mature -- sans la surcharge d'un ramasse-miettes.

Pour FLIN spécifiquement, Rust nous a permis d'aller vite et de construire un logiciel correct simultanément. Le compilateur attrape nos erreurs avant qu'elles n'atteignent les utilisateurs. Le système de types documente notre architecture. Le modèle de propriété prévient les bugs mémoire qui affligent les runtimes de langage. Et Cargo nous donne des builds reproductibles qui fonctionnent de manière identique à Abidjan et partout ailleurs dans le monde.

Chaque compilateur de langage majeur est écrit en C, C++ ou en lui-même. Celui de FLIN sera éventuellement écrit en FLIN. Mais la fondation -- les cinq mille lignes de Rust qui alimentent la machine virtuelle, la base de données et le serveur -- restera. Rust a mérité cette permanence.


Prochain dans la série : Écrire des applications comme en 1995 avec la puissance de 2026 -- FLIN ramène la simplicité du développement web de 1995, mais avec un compilateur, une VM et une base de données derrière chaque ligne de code.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles