Back to flin
flin

Session 1 : mise en place du projet et 42 mots-clés

Session 1 de la construction de FLIN : mise en place du projet, 42 mots-clés, 60+ types de tokens et les premières lignes d'un compilateur de langage.

Thales & Claude | March 30, 2026 15 min flin
EN/ FR/ ES
flinsession-001lexertokenskeywordsrustday-one

Le 1er janvier 2026, nous avons ouvert un terminal à Abidjan et tapé cargo init. FLIN n'existait pas encore. Il n'y avait pas de lexer, pas de parser, pas de système de types, pas de runtime. Il y avait un PRD -- sept documents totalisant des milliers de lignes -- et un répertoire vide. Quarante-cinq minutes plus tard, nous avions un projet Rust avec 14 fichiers, plus de 1 000 lignes de code, 42 mots-clés définis, 60+ types de tokens énumérés, 25 tests unitaires, et le squelette de chaque phase du compilateur mappé vers son propre module. Pas un prototype. Pas un brainstorm. La fondation d'un langage de programmation.

C'est l'histoire de cette première session -- les décisions que nous avons prises avant d'écrire une seule ligne de code, la structure du projet qui porterait FLIN à travers des dizaines de sessions, et les 42 mots-clés qui définissent ce qu'est FLIN.


Pourquoi Rust pour un langage de programmation

Le choix du langage d'implémentation était la première vraie décision. Nous aurions pu écrire le compilateur de FLIN en TypeScript, Python, Go ou C. Nous avons choisi Rust, et le raisonnement était simple.

Un compilateur de langage de programmation est l'un des programmes les plus exigeants que vous puissiez écrire. Il doit être rapide -- les développeurs le lanceront des milliers de fois par jour. Il doit être correct -- un bug dans le compilateur produit des bugs dans chaque programme compilé avec. Et il doit gérer des structures de données complexes -- arbres, graphes, tables de hachage, énumérations récursives -- sans fuites mémoire ni corruption d'état.

Rust vous donne les trois. Le système de types détecte des catégories entières de bugs à la compilation. Le modèle de propriété élimine les fuites mémoire sans ramasse-miettes. Et la performance est à portée du C. Quand vous êtes deux personnes -- un CEO humain, une IA CTO -- construisant un compilateur en partant de zéro, le compilateur Rust est votre coéquipier le plus fiable. Si le code compile, vous pouvez lui faire confiance.

Il y avait aussi une raison pratique. FLIN est conçu pour compiler en bytecode et s'exécuter sur une machine virtuelle personnalisée. Cette VM doit être rapide, embarquable et déployable en binaire unique. Rust produit exactement cela.


Le squelette du projet

Avant d'écrire la moindre logique de compilateur, nous avons posé la structure des modules. Chaque phase du compilateur a obtenu son propre répertoire. Chaque répertoire a reçu un fichier mod.rs qui grandirait au fil des sessions suivantes. Le principe était clair : chaque module possède une phase du pipeline de compilation, ne dépend que de ce dont il a besoin et peut être testé en isolation.

flin-official/
  Cargo.toml              # Manifeste du projet
  src/
    main.rs               # Point d'entrée CLI
    lib.rs                # Exports de bibliothèque
    lexer/
      mod.rs              # Module lexer
      token.rs            # Définitions de tokens
    parser/mod.rs          # Module parser (squelette)
    typechecker/mod.rs     # Vérificateur de types (squelette)
    codegen/mod.rs         # Générateur de code (squelette)
    vm/mod.rs              # Machine virtuelle (squelette)
    database/mod.rs        # Moteur FlinDB / ZEROCORE (squelette)
    server/mod.rs          # Serveur HTTP (squelette)
    error/mod.rs           # Gestion des erreurs
  examples/
    counter.flin           # Exemple compteur de 4 lignes
    todo.flin              # Exemple application todo complète

Sept modules. Sept phases. Chacun un placeholder en attente d'implémentation -- sauf lexer/token.rs, que nous remplirions complètement dans cette session.

Le Cargo.toml était minimal mais délibéré :

toml[package]
name = "flin"
version = "0.1.0"
edition = "2021"
description = "The FLIN programming language compiler and runtime"

[dependencies]
# Phase 0 : Dépendances minimales
# Dépendances supplémentaires ajoutées par phase

[dev-dependencies]
# Utilitaires de test ajoutés selon les besoins

Pas de surcharge de dépendances au premier jour. Chaque phase ajouterait exactement ce dont elle a besoin et rien de plus. C'est un pattern que nous avons appris en construisant sh0.dev : déclarer les dépendances au niveau du workspace, les ajouter aux modules feuilles uniquement quand c'est nécessaire. Moins il y a de pièces mobiles au premier jour, moins il y a de choses qui peuvent casser avant que vous ayez écrit votre première vraie fonctionnalité.


Définir le token : l'atome de la compilation

Le premier travail d'un compilateur est de transformer un flux de caractères en un flux de tokens. Avant de pouvoir construire la machine qui fait cela -- le lexer -- vous devez définir ce qu'est réellement un token.

Dans FLIN, un token est un struct avec trois champs :

rust#[derive(Debug, Clone)]
pub struct Token {
    pub kind: TokenKind,
    pub span: Span,
    pub lexeme: String,
}

#[derive(Debug, Clone, Copy)]
pub struct Span {
    pub start: Position,
    pub end: Position,
}

#[derive(Debug, Clone, Copy)]
pub struct Position {
    pub line: u32,
    pub column: u32,
    pub offset: u32,  // Décalage en octets dans la source
}

Trois niveaux d'information. Le kind vous dit ce qu'est le token -- un mot-clé, un opérateur, un littéral, un identifiant. Le span vous dit où il se trouve dans le fichier source, à la fois en paires ligne/colonne (pour les messages d'erreur lisibles par l'humain) et en décalages d'octets (pour le découpage efficace). Le lexeme est le texte brut qui a été reconnu.

Ce système de double coordonnée -- ligne/colonne plus décalage d'octets -- était une décision de conception délibérée. Les messages d'erreur ont besoin de numéros de ligne ("erreur à la ligne 42, colonne 7"). Mais les opérations internes comme le découpage de chaînes et le mapping de source sont plus rapides avec les décalages d'octets. Transporter les deux coûte quelques octets supplémentaires par token, mais élimine une catégorie entière de bugs de traduction de coordonnées en aval.


Les 42 mots-clés

FLIN n'est pas un langage généraliste. C'est un langage spécialisé pour construire des applications web full-stack avec des opérations de base de données intégrées, des requêtes d'intention alimentées par l'IA, des données temporelles et une couche de vues réactives. Les 42 mots-clés reflètent exactement cette portée. Chaque mot-clé a mérité sa place en représentant un concept que FLIN gère nativement, pas à travers une bibliothèque ou un framework.

Nous les avons organisés en six catégories :

Opérations sur les données (9 mots-clés) : entity, save, delete, where, find, all, first, count, order. Ce sont les verbes de la couche de persistance intégrée de FLIN. Vous n'importez pas d'ORM. Vous n'écrivez pas de SQL. Vous écrivez save user et le compilateur sait quoi faire.

Types (8 mots-clés) : text, int, float, bool, time, file, money, semantic. Le système de types de FLIN est petit et pratique. money est un type de première classe parce que FLIN cible les applications métier en Afrique de l'Ouest où la gestion des devises n'est pas optionnelle. semantic est un modificateur de type pour la recherche vectorielle alimentée par l'IA -- semantic text signifie "ce champ peut être recherché par le sens, pas seulement par correspondance exacte".

Flux de contrôle (5 mots-clés) : if, else, for, in, match. Terrain familier. Pas de boucle while -- FLIN utilise for avec des intervalles et des collections. Pas de switch -- FLIN utilise match avec du pattern matching.

Références temporelles (7 mots-clés) : now, today, yesterday, tomorrow, last_week, last_month, last_year. Le temps n'est pas une réflexion après coup dans FLIN. Chaque entité est automatiquement versionnée. Ces mots-clés sont des références temporelles de première classe que le compilateur comprend au niveau des types.

Intention / IA (4 mots-clés) : ask, search, by, limit. Ils alimentent l'intégration IA de FLIN. ask "Quelles sont les factures en retard ?" est une expression FLIN valide qui compile vers une instruction bytecode invoquant un LLM. search "produits similaires" in Product by description limit 5 effectue une recherche de similarité vectorielle au niveau du langage.

Littéraux et contexte (9 mots-clés) : true, false, none, event, params, body, route, asc, desc. La colle qui tient tout ensemble -- valeurs booléennes, gestion du null, contexte HTTP pour les gestionnaires de routes et direction de tri pour les requêtes.

La recherche de mots-clés a été implémentée comme une expression match directe dans le lexer :

rustfn scan_identifier(&mut self) -> TokenKind {
    while self.peek().map_or(false, |c| c.is_alphanumeric() || c == '_') {
        self.advance();
    }

    let lexeme = self.current_lexeme();

    match lexeme.as_str() {
        "entity" => TokenKind::Keyword(Keyword::Entity),
        "save"   => TokenKind::Keyword(Keyword::Save),
        "delete" => TokenKind::Keyword(Keyword::Delete),
        "where"  => TokenKind::Keyword(Keyword::Where),
        "find"   => TokenKind::Keyword(Keyword::Find),
        "all"    => TokenKind::Keyword(Keyword::All),
        "first"  => TokenKind::Keyword(Keyword::First),
        "count"  => TokenKind::Keyword(Keyword::Count),
        "order"  => TokenKind::Keyword(Keyword::Order),
        // ... 33 autres mots-clés
        "true"   => TokenKind::Keyword(Keyword::True),
        "false"  => TokenKind::Keyword(Keyword::False),
        "none"   => TokenKind::Keyword(Keyword::None),

        _ => TokenKind::Identifier(lexeme),
    }
}

Simple. Un match sur une chaîne. Pas de table de hachage, pas de trie, pas de fonction de hachage parfaite. Aurions-nous pu optimiser cela plus tard avec phf (un crate de map de hachage parfaite à la compilation) ? Oui. En avions-nous besoin au premier jour ? Non. Le compilateur Rust transformera ce match en table de sauts ou recherche binaire. Pour 42 branches, c'est assez rapide pour n'importe quel fichier source réaliste. L'optimisation prématurée dans un compilateur est doublement dangereuse parce que vous écrivez l'outil qui optimise le code des autres -- si vous ne pouvez pas résister à l'envie dans votre propre base de code, vous ne livrerez jamais.


60+ types de tokens

Les mots-clés ne sont qu'une catégorie de tokens. L'enum TokenKind de FLIN a plus de 60 variantes, couvrant chaque élément syntaxique que le langage peut contenir :

Littéraux : Integer(i64), Float(f64), String(String), Bool(bool). Chaque littéral transporte sa valeur parsée directement dans le token. Le lexer ne fait pas qu'identifier que 42 est un nombre -- il le parse en i64 et stocke le résultat. Cela signifie que le parser n'a jamais à re-parser les littéraux, éliminant une source courante de bugs.

Opérateurs : arithmétiques (+, -, *, /, %), comparaison (==, !=, <, <=, >, >=), logiques (&&, ||, !), incrémentation/décrémentation (++, --), assignation (=), temporel (@), optionnel (?) et flèche (->). Vingt-deux tokens d'opérateurs au total.

Délimiteurs : parenthèses, accolades, crochets, virgule, deux-points, point-virgule, point. La ponctuation standard du code structuré.

Tokens de vue : c'est là que FLIN diverge de la plupart des langages. Parce que FLIN a une couche de vue de type HTML intégrée, le lexer doit gérer la syntaxe de balises : TagOpen (<), TagClose (>), TagSelfClose (/>), TagEnd (</), plus les tokens TagName, AttrName et Text pour le contenu à l'intérieur des vues. Le lexer y parvient grâce à une conception modale -- il bascule entre les modes Code, View et ViewExpression quand il rencontre des structures de type HTML.

Tokens de contrôle de vue : {if, {else, {else if, {/if}, {for, {/for}. Ce sont les constructions de flux de contrôle de template qui vivent à l'intérieur de la couche de vue, distinctes des instructions if et for du mode code.

L'enum complet TokenKind est le contrat entre le lexer et le parser. Chaque forme syntaxique que FLIN supporte doit avoir un type de token correspondant. Bien faire cela au premier jour signifiait que le parser -- que nous construirions dans les sessions 5 et 6 -- pouvait faire confiance au fait que son entrée était bien formée et complète.


Les premiers tests

Nous avons écrit 25 tests unitaires dans cette session, tous concentrés sur les définitions de tokens et la reconnaissance de mots-clés. Ce n'étaient pas des tests d'intégration -- nous n'avions pas encore de lexer à tester. Ils testaient les types de tokens eux-mêmes : que chaque mot-clé correspondait à la bonne variante d'enum, que le formatage Display produisait une sortie lisible, que les calculs de span étaient corrects.

Cela peut sembler prématuré. Pourquoi tester des types de données avant d'avoir une logique ? Parce que les types de tokens sont la fondation de chaque phase suivante. Si Keyword::Save se retrouve accidentellement associé à la chaîne "delete" dans une implémentation from_str, vous ne détecterez pas ce bug tant que le parser ne commencera pas à produire des AST erronés, moment auquel vous aurez passé des heures à déboguer la mauvaise couche.

Tester d'abord la fondation est une assurance bon marché. Vingt-cinq tests ont pris quelques minutes à écrire. Ils ont économisé des heures de débogage au cours des sessions suivantes.


Deux programmes exemples

Nous avons aussi écrit deux fichiers .flin d'exemple : un compteur de 4 lignes et une application todo de 55 lignes. Ils n'étaient pas exécutables -- nous n'avions pas encore de compilateur. C'étaient des spécifications. Des programmes concrets contre lesquels chaque phase du compilateur serait testée.

L'exemple du compteur était délibérément minimal :

flincount = 0
<button click={count++}>{count}</button>

Quatre lignes. Une déclaration de variable, un élément HTML, un gestionnaire d'événement avec une expression et une interpolation de texte. Mais ces quatre lignes exercent presque chaque phase du compilateur : analyse lexicale (mots-clés, opérateurs, tokens de vue), parsing (déclaration de variable, élément de vue, expression), vérification de types (inférence d'entier, typage du gestionnaire d'événement) et génération de code (bytecode pour la réactivité et les mises à jour DOM).

Si l'exemple du compteur compile et s'exécute correctement, FLIN fonctionne. Ce seul programme est devenu notre étoile polaire pour les neuf premières sessions.


Ce que nous n'avons pas fait

La session 1 portait autant sur ce que nous avons sauté que sur ce que nous avons construit.

Nous n'avons pas installé Rust sur la machine de développement. La session s'est concentrée entièrement sur la conception et la génération de code. La compilation et les tests viendraient en session 2. C'était un choix de workflow délibéré : définir l'architecture complètement, puis vérifier qu'elle compile. Dans notre workflow CEO-IA CTO, Claude peut générer du code Rust correct sans boucle de retour compilateur -- le raisonnement sur le système de types se fait dans le modèle, pas dans cargo check.

Nous n'avons ajouté aucune dépendance externe au-delà de ce que la bibliothèque standard fournit. Pas de serde pour la sérialisation. Pas de clap pour le parsing d'arguments CLI. Pas de thiserror pour les macros de gestion d'erreurs. Chaque dépendance serait ajoutée dans la session qui en a besoin, justifiée par un cas d'usage spécifique.

Nous n'avons rien optimisé. La recherche de mots-clés est un match linéaire. Le struct token transporte un String alloué sur le tas pour chaque lexème. Le suivi de position utilise des u32 là où des u16 suffiraient. Rien de tout cela ne compte quand vous avez zéro utilisateur et zéro programme à compiler. L'optimisation sans mesure est de la devinette, et deviner n'est pas de l'ingénierie.


La session en chiffres

MétriqueValeur
Durée~45 minutes
Fichiers créés14
Lignes de Rust~1 000
Mots-clés définis42
Types de tokens définis60+
Tests unitaires25
Dépendances externes0
Phases du compilateur échafaudées7
Tâches terminées12 sur 350 au total

Douze tâches sur 350. Trois virgule quatre pour cent du plan d'implémentation total. Mais les bons 3,4 % -- la fondation dont chaque tâche suivante dépend.


Ce qui a rendu cela possible

Construire la couche de tokens d'un langage de programmation en 45 minutes n'est pas typique. Deux facteurs l'ont rendu possible.

Premièrement, le PRD était exhaustif. Avant la session 1, nous avions déjà écrit sept documents de spécification couvrant la syntaxe de FLIN, le système de types, l'architecture du compilateur, le format bytecode, le comportement du runtime et le moteur de base de données. Les définitions de tokens n'ont pas été inventées pendant la session -- elles ont été transcrites à partir d'une spécification qui avait été conçue et relue pendant des semaines. La session était de l'implémentation pure, pas de la conception.

Deuxièmement, le workflow CEO-IA CTO élimine l'écart entre décision et implémentation. Thales a décidé de la structure du projet. Claude l'a implémentée. Il n'y avait pas de changement de contexte, pas de "laissez-moi configurer mon éditeur", pas de "j'ai besoin de lire la doc de ce crate". Toute la session était un flux continu de la décision architecturale au code fonctionnel.


Ce qui a suivi

La session 1 nous a donné les définitions de tokens. Mais les tokens sont des données inertes -- ils ne font rien tant qu'un lexer ne les produit pas à partir du code source. La session 2 installerait Rust, vérifierait la compilation et commencerait à construire le scanner : la machine à états caractère par caractère qui lit les fichiers source FLIN et émet les tokens que nous avons définis aujourd'hui.

Avant d'y arriver, le prochain article examine de plus près le lexer lui-même -- pas seulement les tokens qu'il produit, mais l'algorithme qui les produit. Comment un lexer gère-t-il un langage qui mélange du code impératif avec des vues de type HTML ? Comment basculer entre le mode code et le mode vue sans perdre sa place ? Comment scanner des chaînes, des nombres et des opérateurs multi-caractères en Rust ?

Les réponses sont dans le scanner. C'est le prochain article.


Ceci est la partie 11 de la série "Comment nous avons construit FLIN", qui documente comment un CEO à Abidjan et une IA CTO ont construit un langage de programmation en partant de zéro.

Navigation de la série : - [11] Session 1 : mise en place du projet et 42 mots-clés (vous êtes ici) - [12] Construire un lexer en partant de zéro en Rust - [13] Pratt Parsing : comment FLIN lit votre code - [14] L'arbre syntaxique abstrait : la représentation interne de FLIN - [15] L'inférence de types Hindley-Milner dans un langage personnalisé

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles