Il existe une classe particulière de bugs qui vous fait douter de la réalité. Tout semble correct. Le code compile. Le serveur répond. Aucun message d'erreur n'apparaît nulle part. Et pourtant quelque chose de fondamental est cassé, et la seule preuve est une absence -- la chose que vous attendiez ne se produit tout simplement pas.
Le 3 février 2026, nous avons rencontré l'un de ces bugs dans FLIN. La création d'entités à l'intérieur de fonctions avait silencieusement cessé de fonctionner. Les utilisateurs pouvaient cliquer sur « Add Task » dans l'application todo et rien ne se passait. Pas d'erreur. Pas de crash. Pas d'avertissement. Le bouton était cliqué, la requête partait, le serveur répondait avec {"type":"ok"}, et la liste des tâches restait inchangée. Le silence était le symptôme.
La matrice des symptômes
Le premier indice que quelque chose d'architectural était cassé venait du pattern de ce qui fonctionnait versus ce qui ne fonctionnait pas :
| Opération | Statut | Notes |
|---|---|---|
saveEdit(task) -- modification d'entités existantes | Fonctionnel | Champs mis à jour correctement |
toggleTask(task) -- modification de champs d'entité | Fonctionnel | Toggle booléen persisté |
deleteTask(task) -- suppression d'entités | Fonctionnel | Entités supprimées proprement |
addTask() -- création de nouvelles entités | Cassé | Rien ne se passait du tout |
Modification, toggle, suppression -- tout fonctionnait. Seule la création était cassée. Et la création était la seule opération qui nécessitait de construire un tout nouvel objet entité à partir de rien.
Comprendre le pipeline d'exécution
La VM de FLIN a deux modes d'exécution principaux. La méthode principale execute() exécute le programme bytecode complet du début à la fin. Mais lors du traitement des requêtes d'action -- clics de boutons, soumissions de formulaires -- la VM utilise une méthode différente appelée execute_until_return. Cette méthode exécute le bytecode d'une seule fonction et s'arrête quand cette fonction retourne.
La distinction compte parce que execute_until_return maintient sa propre table de dispatch d'opcodes. C'est, en effet, une mini-VM dans la VM, ne gérant que les opcodes qui peuvent apparaître dans les corps de fonctions. Et c'est là que réside le problème : si un opcode n'est pas dans cette table de dispatch, il n'est pas exécuté. Il tombe dans un cas par défaut qui avance silencieusement le pointeur d'instruction sans rien faire de significatif.
Le gestionnaire manquant
À la Session 269, nous avions ajouté plusieurs gestionnaires d'opcodes à execute_until_return pour supporter le nouveau système d'actions. Chaque opcode nécessaire pour modifier, supprimer et sauvegarder des entités était présent. Mais CreateEntity -- l'opcode qui construit une nouvelle instance d'entité -- n'était pas dans la liste. Il avait été oublié parce que tous nos tests se concentraient sur saveEdit(task), qui modifie des entités existantes en utilisant SetField. Nous n'avions jamais testé addTask() avec le nouveau système d'actions.
Comment l'échec silencieux fonctionne
Le bytecode pour créer et sauvegarder une nouvelle tâche ressemble à ceci :
ip=1742: CreateEntity (0x77) + u16 type_idx + u8 field_count = 4 bytes
ip=1746: StoreLocal (0x21) + u8 slot = 2 bytes
ip=1748: LoadLocal (0x20) + u8 slot = 2 bytes
ip=1750: Save (0x90) = 1 byte
ip=1751: LoadNone + ReturnLa VM n'a jamais atteint Save à ip=1750 car elle a mal interprété les octets d'opérandes de CreateEntity comme des opcodes. L'entité n'a jamais été créée. Pas d'erreur. Pas de crash. Juste le silence.
Le processus de débogage
Trouver ce bug a nécessité trois techniques de débogage complémentaires : le suivi des offsets de bytecode, le traçage d'exécution de la VM, et un compteur d'opérations sur les entités. Quand le compteur était à zéro après une fonction qui aurait dû sauvegarder une entité, nous avions la preuve définitive que le chemin entier de création et de persistance d'entité était sauté.
La correction
La correction elle-même était substantielle mais mécanique -- ajouter le gestionnaire CreateEntity à execute_until_return :
rustOpCode::CreateEntity => {
let type_idx = self.read_u16(code);
let field_count = self.read_u8(code) as usize;
let entity_type = self.get_identifier(chunk, type_idx)?;
// Auto-register entity schema if not already registered
if !self.database.has_entity_type(&entity_type) {
use crate::database::EntitySchema;
let schema = EntitySchema::new(&entity_type);
let _ = self.database.register_entity(schema);
}
let mut entity = EntityInstance::new(entity_type.clone());
// Pop field values and names from the stack
for _ in 0..field_count {
let value = self.pop()?;
let name = self.pop()?;
if let Value::Object(id) = name {
if let Ok(s) = self.get_string(id) {
entity.fields.insert(s.to_string(), value);
}
}
}
let id = self.alloc(HeapObject::new_entity(entity));
self.push(Value::Object(id))?;
}Leçons pour les concepteurs de langages
Ce bug illustre plusieurs principes qui s'appliquent à toute machine virtuelle ou implémentation d'interpréteur.
Premièrement, les échecs silencieux sont la classe de bugs la plus dangereuse. Deuxièmement, les tables de dispatch parallèles doivent rester synchronisées. Troisièmement, testez le flux complet, pas seulement les composants.
L'opcode CreateEntity a été manquant pendant peut-être un jour avant que nous le remarquions. Il a fallu trente minutes pour le corriger une fois le problème identifié, mais ces trente minutes d'investigation étaient une leçon magistrale en débogage systématique.
Ceci est la partie 156 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et construit un langage de programmation à partir de zéro.
Navigation de la série : - Arc précédent : bibliothèque standard et écosystème FLIN - [156] L'opcode CreateEntity qui a disparu (vous êtes ici) - [157] Le bug d'itération de la boucle for