Back to flin
flin

Le format de bytecode expliqué

Le format de bytecode de FLIN : encodage des instructions, pool de constantes, instructions de vues et le binaire .flinc.

Thales & Claude | March 30, 2026 16 min flin
EN/ FR/ ES
flinbytecodeinstruction-setbinary-formatvmflinc

FLIN compile en bytecode. Pas en JavaScript. Pas en WASM. Son propre jeu d'instructions, conçu pour un langage qui se souvient de tout.

La plupart des nouveaux langages de programmation évitent cette décision. Ils compilent en JavaScript (pour le navigateur), en LLVM IR (pour le code natif) ou en WASM (pour la portabilité). Chacune de ces cibles est un choix raisonnable, et chacune s'accompagne d'un écosystème d'outils, d'optimiseurs et de runtimes qui prendrait des années à reproduire.

Nous avons quand même construit notre propre jeu d'instructions.

La raison est que FLIN n'est pas un langage généraliste. C'est un langage avec persistance de données intégrée, voyage dans le temps, vues réactives et requêtes alimentées par l'IA. Aucun format de bytecode existant n'a d'opcodes pour « sauvegarder cette entité dans la base de données », « récupérer cette valeur telle qu'elle existait hier » ou « créer un élément DOM et y lier un noeud de texte réactif ». Nous aurions pu compiler ces opérations en appels de fonctions dans la VM de quelqu'un d'autre, mais cela aurait fait des fonctionnalités fondamentales de FLIN des citoyens de seconde classe -- des appels de bibliothèque au lieu d'instructions natives.

Cet article décrit le format binaire .flinc : comment les instructions sont encodées, comment le pool de constantes est structuré, comment les opérations de vues fonctionnent et comment le format supporte le débogage.

Le format de fichier .flinc

Un programme FLIN compilé est stocké dans un fichier .flinc (FLIN Compiled). Le format commence par un en-tête de 64 octets, suivi de sections de longueur variable :

Offset    Size    Field
------    ----    -----
0x0000    4       Nombre magique : 0x464C494E ("FLIN" en ASCII)
0x0004    1       Version majeure
0x0005    1       Version mineure
0x0006    1       Version de patch
0x0007    1       Drapeaux
0x0008    4       Offset du pool de constantes
0x000C    4       Taille du pool de constantes (nombre d'entrées)
0x0010    4       Offset de la section de code
0x0014    4       Taille de la section de code (octets)
0x0018    4       Offset des informations de débogage (0 si absent)
0x001C    4       Taille des informations de débogage (octets)
0x0020    4       Offset du schéma d'entité
0x0024    4       Taille du schéma d'entité
0x0028    4       Offset de la section de vues
0x002C    4       Taille de la section de vues
0x0030    16      Réservé (mis à zéro)

L'en-tête fait exactement 64 octets -- 0x40 en hexadécimal. Cet alignement est délibéré. Le pool de constantes commence à l'offset 0x40, le rendant facile à localiser par inspection lors de l'examen d'un dump hexadécimal.

La représentation Rust reflète cette disposition :

rust#[repr(C)]
pub struct FlincHeader {
    pub magic: [u8; 4],           // "FLIN"
    pub version_major: u8,
    pub version_minor: u8,
    pub version_patch: u8,
    pub flags: u8,
    pub const_pool_offset: u32,
    pub const_pool_count: u32,
    pub code_offset: u32,
    pub code_size: u32,
    pub debug_offset: u32,
    pub debug_size: u32,
    pub entity_offset: u32,
    pub entity_size: u32,
    pub view_offset: u32,
    pub view_size: u32,
    pub reserved: [u8; 16],
}

Le nombre magique sert deux objectifs : l'identification du type de fichier (pour que les outils puissent rapidement rejeter les fichiers non-FLIN) et la détection de l'ordre des octets. Les octets 0x46 0x4C 0x49 0x4E épellent « FLIN » en ASCII -- si vous ouvrez un fichier .flinc dans un éditeur hexadécimal, les quatre premiers caractères vous disent immédiatement ce que vous regardez.

L'octet de drapeaux encode les métadonnées de la compilation :

rustpub mod Flags {
    pub const DEBUG_INFO: u8      = 0b0000_0001;  // A des informations de débogage
    pub const HAS_VIEWS: u8       = 0b0000_0010;  // A des définitions de vues
    pub const HAS_ENTITIES: u8    = 0b0000_0100;  // A des schémas d'entités
    pub const HAS_ROUTES: u8      = 0b0000_1000;  // A des routes HTTP
    pub const OPTIMIZED: u8       = 0b0001_0000;  // Optimisations appliquées
    pub const WASM_TARGET: u8     = 0b0010_0000;  // Construit pour WASM
}

Un build de développement typique a les drapeaux 0x03 (informations de débogage et vues). Un build de production pourrait avoir 0x16 (vues, entités, optimisé). La VM vérifie ces drapeaux au chargement pour déterminer quelles sections sont présentes et comment initialiser ses sous-systèmes.

Toutes les valeurs multi-octets dans l'en-tête -- et dans l'ensemble du format -- sont stockées en ordre d'octets little-endian.

Le pool de constantes

Le pool de constantes se trouve immédiatement après l'en-tête et stocke chaque valeur qui ne peut pas être encodée directement dans un opérande d'instruction. Chaque entrée est étiquetée avec un identifiant de type d'un octet suivi des données de la valeur :

Tag 0x00 (Null):     [0x00]                          -- 1 octet
Tag 0x01 (Bool):     [0x01] [val]                     -- 2 octets
Tag 0x02 (Int):      [0x02] [i64, little-endian]      -- 9 octets
Tag 0x03 (Float):    [0x03] [f64, little-endian]      -- 9 octets
Tag 0x04 (String):   [0x04] [len: u32] [octets UTF-8] -- 5 + len octets
Tag 0x05 (Identifier): [0x05] [len: u16] [octets UTF-8] -- 3 + len octets
Tag 0x06 (EntityName): [0x06] [len: u16] [octets UTF-8] -- 3 + len octets
Tag 0x07 (Function): [0x07] [arity: u8] [addr: u16] [name_idx: u16] -- 6 octets
Tag 0x08 (Time):     [0x08] [timestamp: i64]          -- 9 octets
Tag 0x09 (Money):    [0x09] [amount: i64] [currency: u8] -- 10 octets

La distinction entre String (tag 0x04) et Identifier (tag 0x05) est subtile mais critique. Les chaînes utilisent un préfixe de longueur u32 parce qu'elles peuvent être arbitrairement longues -- un utilisateur pourrait écrire un littéral de chaîne de plusieurs kilo-octets. Les identifiants utilisent un préfixe de longueur u16 parce que les noms de variables sont courts par convention, et économiser deux octets par identifiant s'accumule quand un programme a des centaines de références de variables.

Le type Money (tag 0x09) mérite une mention spéciale. FLIN est conçu pour les applications en Cote d'Ivoire et à travers l'Afrique de l'Ouest, où la gestion des devises est une préoccupation de premier ordre. Le pool de constantes encode les valeurs monétaires comme un montant entier de 64 bits (dans la plus petite unité de la devise -- centimes pour le XOF, cents pour l'USD) plus un code de devise d'un octet. Pas de représentation en virgule flottante, pas d'erreurs d'arrondi, pas de bogues du type « vous devez 0,30000000000000004 dollars ».

Voici à quoi ressemble un programme simple dans le pool de constantes :

Source : count = 42, name = "Juste"

Pool de constantes :
  [0] Int(42)             -> 02 2A 00 00 00 00 00 00 00
  [1] String("Juste")     -> 04 05 00 00 00 4A 75 73 74 65
  [2] Identifier("count") -> 05 05 00 63 6F 75 6E 74
  [3] Identifier("name")  -> 05 04 00 6E 61 6D 65

Le générateur de code déduplique les constantes au moment de la compilation. Si count apparaît dans dix instructions, le pool de constantes contient une seule entrée Identifier("count"), et les dix instructions référencent l'index 2.

Encodage des instructions

Le bytecode FLIN utilise des instructions de longueur variable. Chaque instruction commence par un opcode d'un octet, éventuellement suivi d'opérandes. Il existe cinq formats d'instruction :

Format 0 -- Pas d'opérande (1 octet) :
  [opcode]
  Exemples : Add, Sub, Mul, Div, Pop, Dup, Return, Halt

Format 1 -- Un opérande u8 (2 octets) :
  [opcode] [u8]
  Exemples : LoadLocal, StoreLocal (emplacements 0-255)

Format 2 -- Un opérande u16 (3 octets) :
  [opcode] [u16 low] [u16 high]
  Exemples : LoadConst, LoadGlobal, Jump, JumpIfFalse

Format 3 -- Un opérande u32 (5 octets) :
  [opcode] [u32 byte0] [u32 byte1] [u32 byte2] [u32 byte3]
  Exemples : JumpFar, CallNative

Format 4 -- Deux opérandes u8 (3 octets) :
  [opcode] [u8] [u8]
  Exemples : Call (arité, const_idx)

L'encodage de longueur variable est un compromis conscient. Les instructions de longueur fixe (comme les instructions de 4 octets d'ARM) simplifient le décodeur et permettent un accès aléatoire dans le flux d'instructions. Les instructions de longueur variable (comme le bytecode x86 ou JVM) produisent une sortie plus compacte au prix d'un décodage séquentiel.

Nous avons choisi la longueur variable pour deux raisons. Premièrement, les programmes FLIN sont petits -- une application typique tient en quelques kilo-octets de bytecode, et économiser des octets compte quand l'ensemble du programme est chargé en mémoire au démarrage. Deuxièmement, les instructions les plus courantes sont les plus courtes. Add, Sub, Pop, Dup, LoadInt0, LoadTrue sont toutes des instructions d'un seul octet. Dans un programme FLIN typique, plus de 60 % des instructions sont de Format 0 (un octet), gardant la longueur moyenne d'instruction bien en dessous de deux octets.

L'espace d'opcodes

Les 256 opcodes possibles sont divisés en 16 plages de 16 opcodes chacune :

0x00 - 0x0F : Flux de contrôle     (Halt, Jump, JumpIfFalse, Call, Return)
0x10 - 0x1F : Opérations de pile   (LoadConst, Pop, Dup, LoadNone, LoadTrue)
0x20 - 0x2F : Variables locales    (LoadLocal, StoreLocal, IncrLocal)
0x30 - 0x3F : Variables globales   (LoadGlobal, StoreGlobal)
0x40 - 0x4F : Arithmétique         (Add, Sub, Mul, Div, Neg, Incr)
0x50 - 0x5F : Comparaison          (Eq, NotEq, Lt, Gt, IsNone)
0x60 - 0x6F : Logique              (And, Or, Not, BitAnd, ShiftLeft)
0x70 - 0x7F : Objets et champs     (CreateObject, GetField, SetField)
0x80 - 0x8F : Listes et maps       (CreateList, GetIndex, MapGet)
0x90 - 0x9F : Opérations d'entités (Save, Delete, QueryAll, QueryFind)
0xA0 - 0xAF : Opérations de vues   (CreateElement, BindText, CreateHandler)
0xB0 - 0xBF : Opérations d'intent  (Ask, Search, Embed)
0xC0 - 0xCF : Opérations temporelles (AtVersion, AtTime, History, LoadNow)
0xD0 - 0xDF : Fonctions intégrées  (Print, ToString, Len, Split, Trim)
0xE0 - 0xEF : Réservé pour extensions
0xF0 - 0xFF : Débogage et spécial  (DebugBreak, SourceLoc, Trace)

Cette disposition révèle le caractère du langage. Les plages 0x00 à 0x8F sont classiques -- tout bytecode basé sur une pile a du flux de contrôle, des opérations de pile, des variables, de l'arithmétique et des structures de données. Mais les plages 0x90 à 0xCF sont uniques à FLIN. Les opérations d'entités, de vues, d'intent (IA) et temporelles sont des catégories d'instructions de première classe, chacune avec sa propre plage de 16 opcodes.

Plus important encore, la plage 0xE0-0xEF est réservée. Cela nous donne 16 opcodes pour les futures fonctionnalités du langage sans casser la compatibilité binaire. Si nous ajoutons le pattern matching, des primitives de concurrence ou de nouveaux types de données, ils ont un emplacement.

Instructions de vues en détail

La plage d'instructions de vues (0xA0-0xAF) implémente le système d'interface utilisateur réactif de FLIN au niveau du bytecode :

OpcodeMnémoniqueDescription
0xA0CreateElementCréer un élément DOM et le pousser sur la pile d'éléments
0xA1CloseElementDépiler l'élément courant de la pile d'éléments
0xA2SetAttributeDéfinir un attribut statique sur l'élément courant
0xA3BindTextLier un noeud de texte réactif à l'élément courant
0xA4BindAttrLier un attribut réactif
0xA5CreateHandlerCommencer un bloc de gestionnaire d'événement
0xA6EndHandlerTerminer un bloc de gestionnaire d'événement
0xA7BindHandlerAttacher le gestionnaire à l'élément courant
0xA8TriggerUpdateSignaler que l'état réactif a changé
0xA9StartIfDébut de bloc de rendu conditionnel
0xAAEndIfFin de bloc de rendu conditionnel
0xABStartForDébut de bloc de rendu en boucle
0xACNextForAvancer à l'itération suivante
0xADEndForFin de bloc de rendu en boucle
0xAEAddTextAjouter du contenu texte statique
0xAFSelfCloseÉlément auto-fermant

Le trio CreateHandler/EndHandler/BindHandler est le mécanisme de gestion des événements. CreateHandler marque le début d'un corps de gestionnaire -- la VM capture les instructions entre CreateHandler et EndHandler comme une unité appelable, similaire à une fermeture. BindHandler attache ce gestionnaire capturé à l'élément courant pour l'événement spécifié.

TriggerUpdate (0xA8) est le pont entre le code impératif et le rendu réactif. Quand un gestionnaire de clic incrémente un compteur, TriggerUpdate indique au système de réactivité de la VM de réévaluer toutes les liaisons qui dépendent de la valeur modifiée. Cette seule instruction remplace toute l'approche de « diff du DOM virtuel » utilisée par des frameworks comme React -- FLIN sait exactement quelles liaisons doivent être mises à jour parce que les dépendances sont enregistrées au niveau du bytecode.

Instructions temporelles et d'intent

La plage temporelle (0xC0-0xCF) implémente la capacité de voyage dans le temps de FLIN :

AtVersion (0xC0): entity, version -> entity    -- entity @ -1
AtTime    (0xC1): entity -> entity              -- entity @ yesterday
AtDate    (0xC2): entity, date_str -> entity    -- entity @ "2024-01-01"
History   (0xC3): entity -> list                -- entity.history
LoadNow   (0xC4): -> time                       -- horodatage courant
LoadToday (0xC5): -> time                       -- début d'aujourd'hui

AtTime utilise un opérande de code temporel d'un octet pour encoder des références temporelles nommées :

0x01: now        0x04: tomorrow
0x02: today      0x05: last_week
0x03: yesterday  0x06: last_month
                 0x07: last_year

Cela signifie que user @ yesterday se compile en deux instructions : LoadGlobal (pour charger l'utilisateur) et AtTime 0x03 (pour demander la version d'hier). La VM résout cela en interrogeant l'index temporel de FlinDB -- chaque mutation d'entité est versionnée, et l'opérateur @ récupère les versions historiques sans que le programmeur n'écrive une seule ligne de logique de requête.

La plage d'intent (0xB0-0xBF) gère les opérations alimentées par l'IA :

Ask         (0xB0): query -> list               -- ask "users from last week"
Search      (0xB1): query, limit -> list        -- search "chair" in Products
SearchMulti (0xB2): query, limit, fields -> list -- recherche multi-champs
Embed       (0xB3): text -> embedding           -- embedding vectoriel

Ces opcodes délèguent au sous-système IA du runtime, qui communique avec les modèles d'embedding et les index de recherche vectorielle. Du point de vue du bytecode, ce sont des instructions ordinaires qui consomment des valeurs de la pile et produisent des résultats. La complexité est cachée derrière la frontière de l'opcode.

Informations de débogage

Les builds de développement incluent une section de débogage qui mappe les offsets de bytecode aux emplacements dans le source :

rustpub struct LineTable {
    entries: Vec<LineEntry>,
}

pub struct LineEntry {
    pub bytecode_offset: u32,
    pub source_line: u32,
    pub source_column: u16,
}

pub struct LocalVarTable {
    entries: Vec<LocalVar>,
}

pub struct LocalVar {
    pub name_idx: u16,       // Index dans le pool de constantes
    pub slot: u8,            // Emplacement de pile
    pub start_offset: u32,   // Début de portée (offset bytecode)
    pub end_offset: u32,     // Fin de portée (offset bytecode)
    pub type_info: u8,       // Étiquette de type
}

La table de lignes permet des messages d'erreur qui pointent vers la bonne ligne source quand une erreur d'exécution survient. La table de variables locales permet au débogueur d'afficher les noms et valeurs des variables quand l'exécution est arrêtée à un point d'arrêt, même si le bytecode lui-même n'utilise que des indices d'emplacements numériques.

La section de débogage est optionnelle. Les builds de production l'omettent entièrement -- le drapeau DEBUG_INFO dans l'en-tête est effacé, et les champs debug_offset et debug_size sont à zéro. Cela garde le bytecode de production minimal.

Un exemple binaire complet

Pour rendre tout cela concret, voici la sortie .flinc complète pour l'exemple du compteur :

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

En hexadécimal :

; En-tête (64 octets)
464C 494E           ; magique : "FLIN"
00 01 00            ; version : 0.1.0
03                  ; drapeaux : DEBUG_INFO | HAS_VIEWS
4000 0000           ; offset pool de constantes : 0x40
0400 0000           ; nombre de constantes : 4
6000 0000           ; offset de code : 0x60
1D00 0000           ; taille de code : 29 octets
...                 ; champs restants de l'en-tête

; Pool de constantes (à 0x40)
02 0000 0000 0000 0000     ; [0] Int(0)
05 0500 636F 756E 74       ; [1] Identifier("count")
05 0600 6275 7474 6F6E     ; [2] Identifier("button")
05 0500 636C 6963 6B       ; [3] Identifier("click")

; Section de code (à 0x60)
10 0000             ; LoadConst 0
31 0100             ; StoreGlobal 1
A0 0200             ; CreateElement 2
A5 0300             ; CreateHandler 3
30 0100             ; LoadGlobal 1
12                  ; Dup
46                  ; Incr
31 0100             ; StoreGlobal 1
A8                  ; TriggerUpdate
A6                  ; EndHandler
A7                  ; BindHandler
30 0100             ; LoadGlobal 1
A3                  ; BindText
A1                  ; CloseElement
00                  ; Halt

Vingt-neuf octets de code. Quatre constantes. Une application compteur réactive avec gestion de clics, gestion d'état et mises à jour automatiques de l'interface utilisateur. L'ensemble de la sortie compilée tient en moins de 150 octets, en-tête inclus.

C'est ce que signifie concevoir un format de bytecode pour un langage spécifique au lieu de cibler une VM généraliste. Chaque instruction est pertinente. Il n'y a pas de décalage d'impédance entre la sémantique du langage et les capacités de la VM. Le bytecode est un encodage direct et compact de l'intention du programmeur.

Décisions de conception et leurs conséquences

Trois décisions ont façonné ce format et auront des conséquences durables.

Basé sur la pile plutôt que sur les registres. Les VM basées sur les registres (comme Lua 5 ou Dalvik) peuvent être plus rapides parce qu'elles évitent les séquences push/pop redondantes. Les VM basées sur la pile (comme la JVM ou CPython) produisent du bytecode plus simple et plus compact et sont plus faciles pour la génération de code. Nous avons choisi la pile parce que le générateur de code a été écrit en une seule session, et la simplicité de la génération de code avait plus de valeur qu'une amélioration de performance de 10-15 % à l'exécution que nous pourrions poursuivre plus tard.

Des opcodes spécifiques au domaine plutôt que des appels de bibliothèque. Faire de Save, QueryAll, CreateElement, AtTime et Ask des opcodes plutôt que des appels de fonctions signifie que la VM peut les distribuer avec un seul bras de match plutôt qu'une configuration de cadre d'appel. Cela signifie aussi que le bytecode est auto-documenté -- un désassemblage d'un programme FLIN se lit comme une description de ce que le programme fait, pas comme un flux d'opérations génériques entrecoupées d'appels de bibliothèque.

Un en-tête fixe de 64 octets plutôt qu'un format de conteneur flexible. Des formats comme ELF ou Mach-O utilisent des tables de sections complexes qui permettent des sections arbitraires. Nous avons utilisé un en-tête fixe avec des offsets de section connus. Cela signifie que l'ajout d'un nouveau type de section nécessite un changement de version du format. Mais cela signifie aussi que le chargement d'un fichier .flinc est une seule lecture de 64 octets suivie de recherches directes par offset -- pas d'analyse de tables de sections, pas d'en-têtes de longueur variable, pas d'ambiguïté.


Ceci est la partie 17 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont construit un compilateur de langage de programmation en sessions mesurées en minutes, pas en mois.

Prochain dans la série : Le sprint qui a construit l'ensemble du compilateur -- dix sessions, deux jours, de zéro à un lexer, parser, vérificateur de types, générateur de code et machine virtuelle fonctionnels.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles