Back to flin
flin

Comment la VM exécute les vues

Comment la VM de FLIN exécute les vues : des opcodes de bytecode au rendu HTML avec liaison d'attributs réactive.

Thales & Claude | March 30, 2026 13 min flin
EN/ FR/ ES
flinviewsrenderinghtmldomvmreactive

La plupart des langages de programmation traitent le rendu HTML comme une réflexion après coup -- un problème de concaténation de chaînes, ou un moteur de templates greffé de l'extérieur. FLIN traite les vues comme du bytecode. La même machine virtuelle qui exécute count = count + 1 exécute aussi <button>Click me</button>. Les instructions de vues sont des opcodes, tout comme l'arithmétique et le flux de contrôle.

Cette décision de conception -- intégrer le rendu de vues dans le jeu d'instructions -- est ce qui fait de FLIN un langage full-stack plutôt qu'un langage backend avec une bibliothèque frontend. Il n'y a pas de compilateur de templates séparé, pas de bibliothèque de DOM virtuel, pas de transformation JSX. Le compilateur FLIN analyse la syntaxe de vues et émet des opcodes de vues. La VM les exécute. Du HTML en sort à l'autre bout.

Cet article couvre comment ce pipeline fonctionne : les opcodes de vues, le tampon de rendu, la liaison d'attributs, la gestion des événements et le pont entre l'exécution du bytecode et le DOM.


Instructions de vues : le jeu d'opcodes

La spécification de bytecode de FLIN réserve la plage 0xA0-0xAF pour les opérations de vues. Il y a 16 opcodes de vues :

0xA0  CreateElement   tag_idx     Créer un nouvel élément DOM
0xA1  CloseElement    --          Fermer l'élément courant
0xA2  SetAttribute    name_idx    Définir un attribut statique
0xA3  BindText        --          Lier un contenu texte réactif
0xA4  BindAttr        name_idx    Lier un attribut réactif
0xA5  CreateHandler   event_idx   Commencer un bloc de gestionnaire d'événement
0xA6  EndHandler      --          Terminer un bloc de gestionnaire d'événement
0xA7  BindHandler     --          Attacher le gestionnaire à l'élément courant
0xA8  TriggerUpdate   --          Déclencher une mise à jour de réactivité
0xA9  StartIf         else_addr   Commencer un bloc de vue conditionnel
0xAA  EndIf           --          Terminer un bloc de vue conditionnel
0xAB  StartFor        end_addr    Commencer un bloc de vue en boucle
0xAC  NextFor         var_slot    Avancer à l'itération suivante
0xAD  EndFor          --          Terminer un bloc de vue en boucle
0xAE  AddText         str_idx     Ajouter du contenu texte statique
0xAF  SelfClose       --          Élément auto-fermant

Ces opcodes transforment la VM en un générateur HTML. Quand la VM rencontre CreateElement, elle ouvre une nouvelle balise HTML. Quand elle rencontre CloseElement, elle ferme la balise. Tout ce qui se trouve entre ces deux instructions devient le contenu de l'élément -- attributs, texte, éléments enfants, gestionnaires d'événements.


Le tampon de rendu

La VM maintient un tampon de rendu interne -- une structure arborescente qui accumule le HTML au fur et à mesure que les instructions de vues s'exécutent :

rustimpl VirtualMachine {
    fn create_element(&mut self, tag: &str) {
        self.view_buffer.push_str(&format!("<{}", tag));
        self.element_stack.push(tag.to_string());
        self.in_open_tag = true;
    }

    fn close_element(&mut self) {
        if self.in_open_tag {
            self.view_buffer.push('>');
            self.in_open_tag = false;
        }
        if let Some(tag) = self.element_stack.pop() {
            self.view_buffer.push_str(&format!("</{}>", tag));
        }
    }

    fn set_attribute(&mut self, name: &str, value: Value) {
        let val_str = self.value_to_html_string(&value);
        self.view_buffer.push_str(&format!(" {}=\"{}\"", name, val_str));
    }
}

La element_stack suit l'imbrication. Quand vous créez un <div>, la chaîne "div" est poussée sur la pile d'éléments. Quand vous le fermez, "div" est dépilé et la balise fermante </div> est émise. Si la pile est vide quand CloseElement s'exécute, la VM signale une erreur de balise non appariée.

C'est un moteur de rendu en flux continu. Il ne construit pas un arbre en mémoire puis le sérialise. Il écrit du HTML directement au fur et à mesure que les instructions s'exécutent. Cela signifie que la sortie est prête dès que l'exécution se termine, sans passe de sérialisation.


Du source FLIN au HTML

Considérez cette vue FLIN :

flincount = 0

view {
    <div class="counter">
        <h1>Counter: {count}</h1>
        <button click={count++}>
            Increment
        </button>
    </div>
}

Le compilateur traduit cela en une séquence d'opcodes de vues :

CreateElement "div"        ; <div
SetAttribute "class"       ; class="counter"
                           ; >

CreateElement "h1"         ; <h1>
AddText "Counter: "        ; Counter:
BindText (LoadGlobal count); {count} -- liaison réactive
CloseElement               ; </h1>

CreateElement "button"     ; <button
CreateHandler "click"      ; onclick="
  LoadGlobal count         ;   count
  Incr                     ;   ++
  StoreGlobal count        ;   count = count + 1
  TriggerUpdate            ;   notifier le système de réactivité
EndHandler                 ;   "
BindHandler                ; attacher le gestionnaire
AddText "Increment"        ; Increment
CloseElement               ; </button>

CloseElement               ; </div>

L'instruction BindText est différente de AddText. AddText émet du texte statique qui ne change jamais. BindText enveloppe le texte dans une liaison réactive -- un <span> avec un attribut data-flin-bind que le runtime côté client peut mettre à jour quand l'état change :

html<span data-flin-bind="count">0</span>

Quand l'utilisateur clique sur le bouton et que count passe de 0 à 1, le JavaScript côté client trouve ce span et met à jour son contenu textuel. Pas de diff de DOM virtuel. Pas de re-rendu complet de la page. Juste une mise à jour ciblée du noeud texte.


Gestionnaires d'événements

Les gestionnaires d'événements dans FLIN sont des blocs de code attachés aux événements DOM. Le compilateur les émet comme une séquence d'instructions entre CreateHandler et EndHandler :

rustInstruction::CreateHandler(event_idx) => {
    let event = self.get_identifier(event_idx);
    self.start_handler(&event);
}

Instruction::EndHandler => {
    self.end_handler();
}

Instruction::BindHandler => {
    self.bind_handler();
}

Le corps du gestionnaire est du bytecode FLIN, mais il ne peut pas être exécuté directement par la VM côté serveur -- il doit s'exécuter dans le navigateur quand l'utilisateur interagit avec la page. Donc la VM traduit le corps du gestionnaire en JavaScript.

Pour un gestionnaire simple comme count++, la traduction est :

javascriptonclick="count++; _flinUpdate()"

L'appel _flinUpdate() à la fin déclenche le système de réactivité pour réévaluer toutes les liaisons. Cela garantit que quand count change, chaque interpolation {count} dans la vue est mise à jour.

Pour des gestionnaires plus complexes impliquant des appels de fonctions ou de la logique conditionnelle, la traduction suit le même pattern : chaque opération FLIN est mappée à son équivalent JavaScript, et _flinUpdate() est ajouté à la fin.


Vues conditionnelles

FLIN supporte le rendu conditionnel avec des blocs {if} :

flinview {
    {if count > 0}
        <p>Count is positive: {count}</p>
    {/if}
}

Le compilateur émet StartIf avec une adresse de saut vers la branche else (ou la fin du bloc) :

LoadGlobal count    ; Pousser count
LoadInt0            ; Pousser 0
Gt                  ; count > 0 ?
StartIf [addr]      ; Si faux, sauter à addr
  CreateElement "p"
  AddText "Count is positive: "
  BindText (count)
  CloseElement
EndIf

Côté serveur, la VM évalue la condition et soit rend le bloc soit le saute. Le HTML initial reflète l'état initial. Côté client, le runtime de réactivité réévalue la condition chaque fois que les variables pertinentes changent et affiche ou cache le bloc en conséquence.


Vues en boucle

La directive {for} itère sur une liste et rend son corps une fois par élément :

flintodos = ["Write code", "Test code", "Ship code"]

view {
    <ul>
        {for todo in todos}
            <li>{todo}</li>
        {/for}
    </ul>
}

La VM exécute cela en :

  1. Évaluant l'expression de liste (todos).
  2. Pour chaque élément, le liant à la variable de boucle (todo).
  3. Exécutant le corps de la boucle (les instructions de vues entre StartFor et EndFor).
  4. Répétant jusqu'à ce que tous les éléments soient traités.
LoadGlobal todos      ; Pousser la liste
StartFor [end_addr]   ; Commencer l'itération
  NextFor 0           ; Lier l'élément courant à l'emplacement local 0
  CreateElement "li"
  BindText (local 0)  ; {todo}
  CloseElement
EndFor

L'instruction NextFor vérifie s'il y a plus d'éléments. Si oui, elle lie l'élément courant à l'emplacement de variable locale spécifié et continue. Si non, elle saute à end_addr pour sortir de la boucle.


Le runtime côté client

La VM côté serveur produit du HTML avec des annotations réactives. Le runtime JavaScript côté client rend ces annotations vivantes. Le runtime est injecté comme un bloc <script> à la fin du HTML rendu :

javascript// FLIN Runtime v0.1
const _state = { count: 0 };

const $flin = new Proxy(_state, {
    set(target, prop, value) {
        target[prop] = value;
        requestAnimationFrame(_flinUpdate);
        return true;
    }
});

Object.defineProperty(window, 'count', {
    get() { return $flin.count; },
    set(v) { $flin.count = v; }
});

function _flinUpdate() {
    document.querySelectorAll('[data-flin-bind]').forEach(el => {
        const expr = el.getAttribute('data-flin-bind');
        el.textContent = eval(expr);
    });
}

Le runtime fait trois choses :

  1. Crée un proxy réactif autour de l'état de l'application. Toute écriture sur une variable d'état déclenche _flinUpdate via requestAnimationFrame.
  2. Expose l'état comme variables globales en utilisant Object.defineProperty. Cela permet aux gestionnaires d'événements de référencer les variables d'état directement (count++) sans préfixe.
  3. Met à jour les liaisons réactives en trouvant tous les éléments avec des attributs data-flin-bind et en réévaluant leurs expressions.

Le batching par requestAnimationFrame est important. Si un gestionnaire met à jour trois variables, le DOM n'est mis à jour qu'une seule fois -- au prochain cadre d'animation. Sans batching, chaque mise à jour de variable déclencherait un parcours DOM séparé.


Le pont entre bytecode et DOM

Le système de vues représente un pont entre deux mondes : la VM Rust qui exécute le bytecode, et le navigateur qui rend le HTML et exécute le JavaScript. Le pont a trois composants :

  1. Au moment de la compilation : le compilateur FLIN analyse la syntaxe de vues et émet des opcodes de vues dans le flux de bytecode. Pas d'étape de compilation de templates séparée.
  2. Au moment du serveur : la VM exécute les opcodes de vues et produit une chaîne HTML avec des annotations réactives (attributs data-flin-bind) et un runtime JavaScript.
  3. Au moment du client : le navigateur rend le HTML. Le runtime JavaScript gère l'interactivité, réévaluant les liaisons quand l'état change.

Cette architecture signifie que les vues FLIN fonctionnent sans JavaScript pour le rendu initial. Le serveur produit du HTML complet. Un robot d'indexation voit du contenu entièrement rendu. Un utilisateur sur une connexion lente voit la page immédiatement, avant que le JavaScript ne charge.

Le JavaScript n'est nécessaire que pour l'interactivité -- cliquer sur des boutons, mettre à jour des compteurs, basculer la visibilité. Et même là, le JavaScript est minimal : un proxy réactif, un pont de variables globales et une fonction de mise à jour du DOM. Pas de framework. Pas de DOM virtuel. Pas d'étape d'hydratation.


Gestion des erreurs dans les vues

Le rendu de vues peut échouer de plusieurs façons : une balise non appariée, une variable manquante dans une interpolation, une erreur de type dans une liaison d'attribut. La VM les gère gracieusement :

  • Balises non appariées : si CloseElement trouve la pile d'éléments vide, ou si le sommet de la pile ne correspond pas à la balise attendue, la VM produit un RuntimeError::ViewError avec les noms de balises non appariés.
  • Variables manquantes : si une instruction BindText référence une globale qui n'existe pas, la VM rend "" (chaîne vide) plutôt que de planter. Cela permet aux vues de se rendre même quand un état n'a pas encore été initialisé.
  • Erreurs de type : si un SetAttribute reçoit une valeur qui ne peut pas être convertie en chaîne (improbable, puisque toutes les valeurs FLIN ont une représentation en chaîne), la VM utilise une chaîne de repli.

Quand le serveur de développement rencontre une erreur de compilation ou de rendu, il n'affiche pas une page blanche. Il génère une page d'erreur stylisée dans le navigateur avec le message d'erreur, le nom du fichier source et le numéro de ligne. Cet affichage d'erreur en ligne signifie que le développeur n'a jamais besoin de basculer vers le terminal pour savoir ce qui s'est mal passé.


Considérations de performance

Le chemin de rendu de vues est optimisé pour le cas courant : principalement du HTML statique avec quelques liaisons réactives.

Les attributs statiques (class="counter") sont émis directement comme chaînes HTML. Pas de surcoût à l'exécution. Pas de suivi réactif. Pas d'implication JavaScript.

Les liaisons réactives ({count}) sont enveloppées dans des spans annotés. Le runtime JavaScript ne traverse que les éléments avec des attributs data-flin-bind, ignorant la vaste majorité du DOM.

Les gestionnaires d'événements sont inlinés comme attributs HTML (onclick="..."). Il n'y a pas de délégation d'événements, pas de système d'événements synthétiques, pas d'enveloppement d'objet événement. La gestion native des événements du navigateur est assez rapide, et éviter les couches d'abstraction garde le code côté client minuscule.

Le runtime JavaScript total fait moins de 50 lignes. Il charge en millisecondes et ajoute un surcoût négligeable aux interactions de la page.


Le pipeline compile-rend-sert

Pour voir le pipeline complet de rendu de vues en action, tracez le chemin du source au navigateur :

  1. Compiler : le compilateur FLIN lit le fichier .flin, le lexe, l'analyse (y compris les blocs de vues), vérifie les types et émet du bytecode avec des opcodes de vues entremêlés parmi les opcodes réguliers.
  1. Exécuter : la VM exécute le bytecode. Quand elle atteint les opcodes de vues, elle construit du HTML dans le tampon de rendu tout en évaluant simultanément les expressions (pour les liaisons réactives) et en traduisant les gestionnaires d'événements (pour la sortie JavaScript).
  1. Envelopper : le HTML rendu est enveloppé dans un document HTML complet : <html>, <head> avec métadonnées, <body> avec le contenu rendu, et un bloc <script> contenant le runtime réactif.
  1. Servir : le serveur HTTP retourne ce document HTML complet en réponse à une requête du navigateur.
  1. Hydrater : le navigateur rend le HTML statique immédiatement. Le runtime réactif attache les écouteurs d'événements et configure le proxy réactif. À partir de ce point, la page est interactive.

Ce pipeline s'exécute en moins de 20 millisecondes pour une application FLIN typique. Le résultat est une page web entièrement interactive générée à partir d'un seul fichier source, sans étape de build, sans bundling, sans dépendances externes.


Ce que les opcodes de vues ont rendu possible

Avec les opcodes de vues dans la VM, FLIN est devenu un langage full-stack. Un seul fichier .flin pouvait définir des données (entités), de la logique (fonctions) et de la présentation (vues). Le développeur écrit un fichier, et FLIN gère tout : compilation, rendu, service et mise à jour.

Pas de webpack. Pas de Vite. Pas de Babel. Pas de React. Pas de Svelte. Pas de pipeline de build avec douze fichiers de configuration. Juste flin dev app.flin, et une application web fonctionnelle apparaît dans le navigateur.

C'est la promesse du système de vues de FLIN. Et tout commence avec seize opcodes dans une VM Rust.


Ceci est la partie 24 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont construit un langage de programmation à partir de zéro.

Prochain : [25] La référence complète des opcodes FLIN -- chaque instruction du bytecode de FLIN, documentée avec les effets de pile et des exemples.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles