Lors de la Session 050, le 5 janvier 2026, nous avons triplé le nombre de méthodes de chaînes dans FLIN. Nous sommes passés de 11 opérations basiques -- celles que chaque langage possède -- à 31 fonctions complètes de manipulation de texte qui couvrent tout ce dont un développeur web a besoin. Recherche. Transformation. Validation. Remplissage. Découpage. Inversion. Le tout sans importer une seule bibliothèque.
Ce n'était pas un exercice théorique. FlinUI avait besoin de starts_with pour détecter les préfixes d'icônes. La validation de formulaires avait besoin de is_numeric et is_email. Le formatage de texte avait besoin de capitalize et title. Chaque méthode de chaîne manquante était un vrai bloqueur pour une vraie fonctionnalité. La Session 050 les a toutes supprimées en un seul passage.
Le point de départ : 11 méthodes qui n'étaient pas suffisantes
Avant la Session 050, FLIN avait 11 méthodes de chaînes, implémentées comme des opcodes directs dans la VM :
flintext.len // Longueur
text.upper // Majuscules
text.lower // Minuscules
text.trim // Supprimer les espaces
text.contains("sub") // Vérifier la sous-chaîne
text.starts_with("pre") // Vérifier le préfixe
text.ends_with("suf") // Vérifier le suffixe
text.split(",") // Découper en liste
text.slice(0, 5) // Extraire une sous-chaîne
text.replace("old", "new") // Remplacer la première occurrence
"-".join(["a", "b"]) // Joindre une liste avec un séparateurCes 11 méthodes existaient parce que nous en avions besoin pour les premières démos de FLIN. Elles étaient implémentées comme des opcodes dédiés dans le bytecode -- chacun un seul octet que la VM matchait et exécutait directement. Elles étaient rapides, correctes, et insuffisantes.
Dès que nous avons commencé à construire les composants FlinUI, les lacunes sont devenues évidentes. Le composant Icon avait besoin de vérifier si un nom d'icône commençait par un préfixe spécifique et de le supprimer. Cela nécessitait remove_prefix -- une fonction que nous n'avions pas. Le composant FormField avait besoin de valider les numéros de téléphone. Cela nécessitait is_numeric -- une autre fonction que nous n'avions pas. Le composant Autocomplete avait besoin de trouver la position d'une correspondance dans une chaîne. Cela nécessitait index_of -- encore une lacune.
Nous aurions pu implémenter chaque fonction manquante une par une, au fur et à mesure du besoin. Au lieu de cela, nous nous sommes assis, avons catalogué chaque opération de chaîne que JavaScript, Python et Rust offrent, et avons demandé : lesquelles un développeur web utilise-t-il réellement ?
La réponse était 20 méthodes supplémentaires. La Session 050 les a toutes implémentées en une seule session.
Les 20 nouvelles méthodes
Méthodes de recherche
flintext.index_of("sub") // Position de la première occurrence, ou none
text.last_index_of("sub") // Position de la dernière occurrence, ou none
text.count("sub") // Compter toutes les occurrencesindex_of et last_index_of retournent la position en caractères (pas en octets -- les chaînes FLIN sont toujours sûres UTF-8) d'une sous-chaîne, ou none si non trouvée. La distinction avec contains est cruciale : contains vous dit si une sous-chaîne existe ; index_of vous dit où elle se trouve.
count était étonnamment courant dans notre analyse. Compter les occurrences d'un caractère dans une chaîne -- virgules dans une ligne CSV, sauts de ligne dans un bloc de texte, voyelles dans un mot -- apparaissait dans la logique de template, la validation et le traitement de données.
Accès aux caractères
flintext.char_at(0) // Premier caractère en tant que texte
text.chars // Liste de caractères individuelsCes deux méthodes comblent une lacune qui cause des bugs dans chaque langage avec des chaînes indexées par octets. En JavaScript, "cafe\u0301"[4] retourne un accent combinant, pas la lettre "e". En FLIN, char_at retourne toujours un graphème Unicode complet. Et chars retourne une liste de caractères individuels, gérant correctement les séquences multi-octets.
flinword = "cafe"
first = word.char_at(0) // "c"
all = word.chars // ["c", "a", "f", "e"]Transformations de chaînes
flintext.repeat(3) // "ab" -> "ababab"
text.reverse // "hello" -> "olleh"
text.capitalize // "hELLO" -> "Hello"
text.title // "hello world" -> "Hello World"capitalize met en minuscules chaque caractère sauf le premier, qu'il met en majuscule. title fait la même chose pour chaque mot. Ce sont des fonctions essentielles pour l'affichage dans l'interface -- montrer les noms d'utilisateurs, générer des titres de pages, formater des étiquettes. En JavaScript, il n'y a pas de capitalize ou titleCase intégré. Les développeurs écrivent les leurs (mal) ou installent lodash pour une seule fonction.
reverse est compatible Unicode. Il inverse la chaîne par caractères, pas par octets. "cafe".reverse produit "efac", pas une séquence d'octets corrompue.
Variantes de découpage
flintext.trim_start // Supprimer les espaces en début
text.trim_end // Supprimer les espaces en finLe trim original supprimait les espaces des deux côtés. Ces variantes donnent un contrôle fin. trim_start est essentiel pour traiter du texte indenté (comme des blocs de code ou du markdown). trim_end est essentiel pour nettoyer les entrées utilisateur qui ont des espaces en fin suite à un copier-coller.
Remplissage
flintext.pad_start(5, "0") // "42" -> "00042"
text.pad_end(10, " ") // "hi" -> "hi "Le remplissage est une de ces fonctions qui semble triviale jusqu'à ce que vous en ayez besoin. Numéros de facture : id.pad_start(8, "0"). Colonnes de tableau à largeur fixe : name.pad_end(20, " "). Affichage d'heure : hours.pad_start(2, "0"). Chaque application web a besoin de remplissage quelque part, et l'écrire à la main est étonnamment sujet aux erreurs (les erreurs de décalage dans la longueur de remplissage sont universelles).
Méthodes de validation
flintext.is_empty // true si ""
text.is_numeric // true si tous des chiffres
text.is_alpha // true si toutes des lettres
text.is_alphanumeric // true si lettres et chiffres uniquementCes quatre méthodes de validation remplacent un nombre impressionnant de patterns regex. Dans notre analyse de code, le pattern /^\d+$/ (tous des chiffres) apparaissait 23 fois dans trois projets. Le pattern /^[a-zA-Z]+$/ (toutes des lettres) apparaissait 11 fois. À chaque fois, un développeur écrivait une regex, la testait et espérait qu'elle gère les cas limites correctement. En FLIN, text.is_numeric est une fonction Rust compilée qui gère chaque cas limite -- y compris la chaîne vide (retourne false) et les chiffres Unicode (configurable).
Suppression de préfixe et suffixe
flintext.remove_prefix("hello_") // "hello_world" -> "world"
text.remove_suffix(".txt") // "file.txt" -> "file"Ce sont les méthodes qui ont déclenché la Session 050. Le composant Icon de FlinUI avait besoin de supprimer un préfixe des noms d'icônes pour diriger vers le bon moteur de rendu d'icônes. Sans remove_prefix, le composant devait utiliser slice avec un décalage codé en dur -- fragile, illisible et incorrect si la longueur du préfixe changeait.
flin// Avant la Session 050 (fragile)
icon_name = props.icon
{if icon_name.starts_with("lucide-")}
actual_name = icon_name.slice(7) // Nombre magique ! Casse si le préfixe change
{/if}
// Après la Session 050 (correct)
icon_name = props.icon
{if icon_name.starts_with("lucide-")}
actual_name = icon_name.remove_prefix("lucide-")
{/if}Opérations sur les lignes
flintext.split_lines // Découper par sauts de lignesplit_lines gère \n, \r\n et \r uniformément. C'est une source constante de bugs inter-plateformes dans d'autres langages. Du code collé depuis Windows a des fins de ligne \r\n. Du code depuis macOS a \n. Du code depuis d'anciens systèmes a \r. split_lines gère les trois et retourne une liste propre de lignes sans aucun caractère de fin de ligne.
L'implémentation : 600 lignes de Rust
Chaque nouvelle méthode nécessitait des changements à quatre endroits : la définition du bytecode, l'exécution dans la VM, l'émetteur et le vérificateur de types. L'architecture était déjà en place grâce aux 11 méthodes originales. Ajouter 20 de plus était une question de suivre le pattern.
Nouveaux opcodes
Chaque méthode de chaîne correspond à un opcode dédié dans le format bytecode :
0x34: IndexOf 0x3A: TrimEnd 0x4A: IsAlphanumeric
0x35: LastIndexOf 0x3B: PadStart 0x4B: Capitalize
0x36: CharAt 0x3C: PadEnd 0x4C: TitleCase
0x37: StringRepeat 0x3D: IsEmpty 0x4D: StringCount
0x38: StringReverse 0x3E: IsNumeric 0x4E: SplitLines
0x39: TrimStart 0x3F: IsAlpha 0x4F: Chars
0xCF: StringSlice 0x59: RemovePrefix 0x5A: RemoveSuffixVingt et un nouveaux opcodes (incluant un opcode StringSlice dédié qui remplace l'opération slice générique pour les chaînes). Chaque opcode fait un seul octet, donc le bytecode reste compact.
Exécution dans la VM
L'implémentation dans la VM pour chaque méthode suit le même pattern : dépiler les arguments, dépiler la chaîne, effectuer l'opération, empiler le résultat. Voici un exemple représentatif -- capitalize :
rustfn exec_string_capitalize(&mut self) -> Result<(), VmError> {
let string_id = self.pop_string()?;
let s = self.heap.get_string(string_id);
let result = if s.is_empty() {
String::new()
} else {
let mut chars = s.chars();
let first = chars.next().unwrap().to_uppercase().to_string();
let rest: String = chars.collect::<String>().to_lowercase();
format!("{}{}", first, rest)
};
let result_id = self.heap.alloc_string(result);
self.push(Value::Object(result_id));
Ok(())
}Douze lignes de Rust. Gère le cas limite de la chaîne vide. Produit une capitalisation Unicode correcte (pas juste ASCII). Alloue le résultat sur le tas et le pousse sur la pile de valeurs. Chaque méthode de chaîne suit exactement ce pattern.
Intégration de l'émetteur
L'émetteur reconnaît les méthodes de chaîne pendant la génération de code et les route vers l'opcode approprié :
rustfn try_emit_string_method(
&mut self,
method_name: &str,
arg_count: usize,
) -> Option<()> {
match (method_name, arg_count) {
("upper", 0) => self.emit_byte(Op::StringUpper),
("lower", 0) => self.emit_byte(Op::StringLower),
("trim", 0) => self.emit_byte(Op::StringTrim),
("capitalize", 0) => self.emit_byte(Op::Capitalize),
("title", 0) => self.emit_byte(Op::TitleCase),
("index_of", 1) => self.emit_byte(Op::IndexOf),
("pad_start", 2) => self.emit_byte(Op::PadStart),
// ... 24 entrées de plus
_ => return None,
}
Some(())
}L'instruction match vérifie à la fois le nom de la méthode et le nombre d'arguments. Cela empêche l'ambiguïté : count avec zéro argument retourne la longueur de la chaîne, tandis que count avec un argument compte les occurrences de sous-chaîne. Le système de types impose cela à la compilation, mais l'émetteur le vérifie à nouveau lors de la génération de code.
Mises à jour du vérificateur de types
Le vérificateur de types doit connaître la signature de chaque méthode pour valider les appels et inférer les types de retour :
rust// Dans check_member() pour FlinType::Text
match method_name {
"upper" | "lower" | "trim" | "capitalize" | "title"
| "trim_start" | "trim_end" | "reverse" => {
FlinType::Function(vec![], Box::new(FlinType::Text))
}
"contains" | "starts_with" | "ends_with"
| "is_empty" | "is_numeric" | "is_alpha" | "is_alphanumeric" => {
FlinType::Function(vec![], Box::new(FlinType::Bool))
}
"index_of" | "last_index_of" | "count" => {
FlinType::Function(vec![FlinType::Text], Box::new(FlinType::Int))
}
"split" | "chars" | "split_lines" => {
FlinType::Function(vec![], Box::new(FlinType::List(Box::new(FlinType::Text))))
}
// ...
}C'est là que la magie d'un langage typé statiquement paie. Si vous écrivez "hello".upper(42), le vérificateur de types le rejette à la compilation -- upper prend zéro argument, pas un. Si vous écrivez name.index_of(42), le vérificateur de types le rejette -- index_of prend un argument text, pas un int. Ces erreurs n'atteignent jamais la VM.
La question UTF-8
L'indexation de chaînes est l'un des domaines les plus traîtres de la conception de langages de programmation. Le problème fondamental : la longueur en octets et la longueur en caractères d'une chaîne UTF-8 sont différentes.
FLIN prend une décision claire : toute indexation de chaîne se fait par caractère, pas par octet. slice(0, 2) retourne les deux premiers caractères, pas les deux premiers octets. char_at(0) retourne le premier caractère, pas le premier octet. len retourne le nombre de caractères, pas le nombre d'octets.
flinword = "cafe"
word.len // 4 (caractères, pas octets)
word.char_at(0) // "c"
word.slice(0, 2) // "ca"
word.chars // ["c", "a", "f", "e"]C'est plus lent que l'indexation par octets -- la VM doit itérer à travers les octets UTF-8 pour trouver les limites de caractères -- mais c'est correct. Et la correction compte plus que la micro-optimisation quand votre langage cible des développeurs web qui travaillent avec du texte dans des dizaines de langues.
Cas d'utilisation qui ont guidé la conception
Chaque méthode a été ajoutée à cause d'un cas d'utilisation concret, pas parce qu'elle existait dans un autre langage :
Dispatch d'icônes FlinUI : icon.starts_with("lucide-") et icon.remove_prefix("lucide-") -- le composant qui a déclenché toute la session.
Validation de formulaires : input.is_empty, phone.is_numeric, email.contains("@") -- trois vérifications qui apparaissent dans chaque composant de formulaire.
Formatage de texte : name.capitalize, title.title, id.pad_start(5, "0") -- formatage d'affichage pour le texte visible par l'utilisateur.
Traitement de données : csv_line.split(","), multiline.split_lines, text.count("\n") -- analyse et traitement de données textuelles.
Construction de chaînes : "ab".repeat(3), word.reverse, items.join(", ") -- construction de chaînes à partir de parties.
Chaînage de méthodes en pratique
La vraie puissance de 31 méthodes de chaînes émerge quand vous les chaînez. Chaque méthode retourne une nouvelle chaîne (ou une liste, ou un booléen), donc les chaînes peuvent être arbitrairement longues :
flin// Nettoyer et formater l'entrée utilisateur
clean_name = raw_input
.trim
.lower
.replace(" ", " ")
.title
// Générer un slug d'URL
slug = article_title
.lower
.trim
.replace(" ", "-")
.replace("--", "-")
// Analyser un en-tête CSV
columns = header_line
.trim
.split(",")
.map(col => col.trim.lower.snake_case)
// Valider et formater un numéro de téléphone
is_valid = phone
.trim
.remove_prefix("+")
.is_numericChaque chaîne se compile en une séquence d'opcodes. Il n'y a pas d'objet pipeline intermédiaire, pas de protocole d'itérateur, pas de framework d'évaluation paresseuse. Chaque méthode s'exécute immédiatement, produit un résultat, et la méthode suivante opère sur ce résultat. Simple. Prévisible. Rapide.
Ce que 31 méthodes ont remplacé
Après la Session 050, nous avons audité les trois projets de référence que nous avions analysés précédemment. Les résultats étaient frappants :
- Expressions régulières éliminées : 47 patterns regex remplacés par des appels de méthodes intégrées
- Fonctions utilitaires éliminées : 23 fonctions utilitaires de chaînes personnalisées remplacées par des intégrées
- Appels de bibliothèques tierces éliminés : 89 appels à des méthodes de chaînes lodash/underscore
- Lignes de code économisées : environ 340 lignes à travers trois projets
Le pattern le plus fréquemment remplacé était la séquence "trim, lowercase, check" qui apparaît dans chaque implémentation de recherche :
flin// Avant : fonction personnalisée + regex
fn normalize(text) {
text.trim.lower.replace(regex("[^a-z0-9]"), "")
}
// Après : chaîne de méthodes
normalized = text.trim.lowerTrente et une méthodes. Six cents lignes de Rust. Zéro import requis. Chaque opération de chaîne dont un développeur web a besoin, disponible dès la première ligne de chaque programme FLIN.
Ceci est la partie 72 de la série "How We Built FLIN", documentant comment un CEO à Abidjan et un CTO IA ont construit un langage de programmation avec une bibliothèque complète de manipulation de chaînes intégrée dans le runtime.
Navigation de la série : - [71] 409 Built-in Functions: The Complete Standard Library - [72] 31 String Methods Built Into the Language (vous êtes ici) - [73] Math, Statistics, and Geometry Functions - [74] Time and Timezone Functions