Les sessions 152 et 153 ont ajouté deux fonctionnalités qui semblent modestes en portée mais changent fondamentalement le fonctionnement de l'itération dans FLIN : les boucles while-let et le break avec valeur. Ensemble, elles apportent un flux de contrôle inspiré de Rust à un langage conçu pour les développeurs d'applications.
La motivation était pratique. Avant ces fonctionnalités, consommer un itérateur ou traiter un flux de valeurs optionnelles nécessitait un déballage manuel maladroit et un contrôle de boucle explicite. Après ces fonctionnalités, l'approche par motifs gère automatiquement le déballage, et les boucles peuvent produire des valeurs tout comme les expressions match et les chaînes if-else.
While-Let : itération pilotée par motifs
Une boucle while-let continue d'itérer tant qu'un motif correspond :
flinitems = [Some(1), Some(2), None, Some(4)]
index = 0
while let Some(value) = items[index] {
print(value)
index++
}
// Prints: 1, 2
// Stops at None because the pattern Some(value) does not matchLa condition de boucle n'est pas une expression booléenne. C'est un filtrage par motif. À chaque itération, le côté droit est évalué et filtré par rapport au motif. Si le motif correspond, le corps de la boucle s'exécute avec les liaisons du motif dans la portée. Si le motif ne correspond pas, la boucle se termine.
C'est du sucre syntaxique pour un pattern courant :
flin// Without while-let
while true {
result = items[index]
if result is None { break }
value = result.unwrap()
print(value)
index++
}
// With while-let
while let Some(value) = items[index] {
print(value)
index++
}La version while-let est plus courte, mais plus important encore, elle est plus sûre. Le unwrap dans la version manuelle est une source potentielle d'erreurs à l'exécution. La version while-let ne fait jamais de unwrap -- elle déstructure, ce qui est vérifié statiquement.
Représentation AST
La boucle while-let est représentée comme un nouveau variant dans l'enum des instructions :
rustStmt::WhileLet {
pattern: Pattern,
value: Expr,
body: Block,
span: Span,
}Le pattern est le côté gauche du = dans la condition. La value est le côté droit. Le body est le corps de la boucle, exécuté à chaque itération où le motif correspond.
Implémentation de l'analyseur syntaxique
L'analyseur détecte while-let en vérifiant le mot-clé let après while :
rustfn parse_while_stmt(&mut self) -> Result<Stmt, ParseError> {
self.expect_keyword("while")?;
if self.check_keyword("let") {
self.advance(); // consume "let"
return self.parse_while_let();
}
// Regular while loop
let condition = self.parse_expression()?;
let body = self.parse_block()?;
Ok(Stmt::While { condition, body, span: self.current_span() })
}
fn parse_while_let(&mut self) -> Result<Stmt, ParseError> {
let pattern = self.parse_pattern()?;
self.expect(&Token::Equals)?;
let value = self.parse_expression()?;
let body = self.parse_block()?;
Ok(Stmt::WhileLet { pattern, value, body, span: self.current_span() })
}La séquence de deux mots-clés while let est non ambiguë parce que let n'est pas un début d'expression valide. L'analyseur peut distinguer entre while condition { ... } et while let pattern = expr { ... } avec un seul jeton d'avance.
Vérification de types
Le vérificateur de types valide que le motif est compatible avec le type de la valeur :
rustfn check_while_let(&mut self, pattern: &Pattern, value: &Expr, body: &Block, span: Span) {
let value_type = self.infer_type(value);
match pattern {
Pattern::EnumVariant { enum_name, variant, inner, .. } => {
let enum_type = self.resolve_type(enum_name);
self.validate_variant(enum_type, variant, inner);
self.push_scope();
if let Some(inner) = inner {
let data_type = self.get_variant_data_type(enum_type, variant);
self.check_pattern(inner, &data_type, span);
}
self.check_block(body);
self.pop_scope();
}
_ => {
self.push_scope();
self.check_pattern(pattern, &value_type, span);
self.check_block(body);
self.pop_scope();
}
}
}Le corps est vérifié dans une nouvelle portée où les liaisons du motif sont disponibles. Quand la boucle se termine (parce que le motif ne correspond plus), les liaisons sortent de la portée.
Génération de code
Le bytecode pour une boucle while-let combine le filtrage par motif avec le contrôle de boucle :
rustfn emit_while_let(&mut self, pattern: &Pattern, value: &Expr, body: &Block) {
let loop_start = self.current_offset();
// Evaluate the value expression
self.emit_expr(value);
let value_local = self.allocate_temp();
self.emit_store(value_local);
// Try to match the pattern
self.emit_load(value_local);
let exit_jump = self.emit_pattern_check(pattern);
// Pattern matched -- bind variables and execute body
self.emit_pattern_bindings(pattern, value_local);
self.emit_block(body);
// Jump back to loop start
self.emit_jump_back(loop_start);
// Patch exit jump to here
self.patch_jump(exit_jump);
}La structure est : évaluer, vérifier le motif, si pas de correspondance sauter à la fin, sinon exécuter le corps et revenir au début. C'est le pattern standard de bytecode boucle-avec-condition, sauf que la condition est un filtrage par motif au lieu d'un test booléen.
Break avec valeur
La session 153 a ajouté la capacité pour break de porter une valeur :
flinresult = for item in items {
if item.matches(criteria) {
break item
}
}Le break item sort de la boucle et produit item comme valeur de la boucle. La boucle est une expression qui s'évalue à la valeur passée à break.
Ceci est directement inspiré du break avec valeur de Rust, et élimine un pattern courant :
flin// Without break-with-value
found = none
for item in items {
if item.matches(criteria) {
found = item
break
}
}
// With break-with-value
found = for item in items {
if item.matches(criteria) {
break item
}
}La seconde version est plus courte et communique l'intention plus clairement. Le but de la boucle est de trouver une valeur, et break item rend cela explicite.
Modifications AST pour Break avec valeur
L'instruction break a gagné une valeur optionnelle :
rustStmt::Break {
value: Option<Expr>,
label: Option<String>,
span: Span,
}Quand value est Some(expr), le break porte une valeur. Quand c'est None, c'est un break simple. Le champ label (discuté dans l'article suivant) permet de sortir des boucles externes.
Vérification de types du Break avec valeur
Quand une boucle est utilisée comme expression, le vérificateur de types doit déterminer son type de résultat. Le type de résultat vient des instructions break :
rustfn check_loop_as_expression(&mut self, loop_stmt: &Stmt) -> FlinType {
let break_types = self.collect_break_types(loop_stmt);
if break_types.is_empty() {
return FlinType::Optional(Box::new(FlinType::Unknown));
}
let mut result_type = break_types[0].clone();
for bt in &break_types[1..] {
result_type = self.unify(&result_type, bt);
}
FlinType::Optional(Box::new(result_type))
}Le type de résultat est optionnel parce que la boucle pourrait exécuter zéro itérations (si la collection est vide) ou pourrait ne pas trouver d'élément correspondant. Dans les deux cas, aucun break ne s'exécute, et la boucle produit none.
flinresult: int? = for item in items {
if item > 10 {
break item
}
}
if result {
print("Found: " + text(result))
} else {
print("No item > 10 found")
}Génération de code pour Break avec valeur
rustfn emit_break_with_value(&mut self, value: &Option<Expr>) {
if let Some(expr) = value {
self.emit_expr(expr);
self.emit_store(self.current_loop_result_slot());
}
self.emit_jump_to_loop_exit();
}Chaque boucle qui pourrait produire une valeur alloue un slot de résultat sur la pile. Quand break value s'exécute, la valeur est stockée dans ce slot. Après la sortie de la boucle, le slot de résultat contient la valeur de la boucle (ou none si aucun break ne s'est exécuté).
While-Let avec Break à valeur
While-let et break avec valeur se combinent naturellement :
flinresult = while let Some(item) = iterator.next() {
if item.is_special() {
break item.transform()
}
process(item)
}La boucle while-let itère tant que l'itérateur produit Some(item). À l'intérieur de la boucle, si un élément spécial est trouvé, la boucle sort avec une valeur transformée. Sinon, l'élément est traité et la boucle continue.
Patterns pratiques
Trouver la première correspondance
flinfirst_admin = for user in users {
if user.role == "admin" {
break user
}
}Accumuler jusqu'à une condition
flintotal = 0
batch = while let Some(item) = queue.dequeue() {
total = total + item.value
if total > threshold {
break total
}
}Analyser avec des jetons optionnels
flintokens = tokenize(input)
index = 0
ast = while let Some(token) = tokens.get(index) {
if token.kind == "EOF" {
break ast_builder.finish()
}
ast_builder.consume(token)
index++
}Traiter des résultats d'API paginés
flinall_results: [User] = []
page = 1
while let Some(batch) = fetch_page(page) {
all_results = all_results + batch
if batch.len < page_size {
break // Last page (not enough results to fill a page)
}
page++
}Décisions de conception
Les motifs while-let sont limités aux motifs réfutables. Tous les motifs n'ont pas de sens dans un while-let. Les motifs irréfutables (comme un simple identifiant) correspondraient toujours, créant une boucle infinie. Le compilateur avertit des motifs while-let irréfutables :
flin// Warning: irrefutable pattern in while-let (this loop will never exit via pattern failure)
while let x = get_value() {
// x always matches -- this is probably a bug
}Le type de la valeur du break doit être cohérent. Si une boucle a plusieurs instructions break avec des valeurs, toutes les valeurs doivent avoir des types compatibles :
flinresult = for item in items {
if item.is_number() {
break item.as_int() // int
}
if item.is_text() {
break item.as_text() // text
}
}
// result: (int | text)?Le compilateur unifie les types de break. S'ils sont différents, le résultat est un type union, enveloppé dans un optionnel.
Le break simple fonctionne toujours. Ajouter break avec valeur ne change pas le comportement du break simple. Un break sans valeur sort de la boucle sans produire de valeur (le résultat est none).
Le parallèle avec Rust
Ces fonctionnalités sont directement inspirées de Rust. Le pattern while let de Rust, son break avec valeur dans les blocs loop, et sa sémantique de boucle-comme-expression ont été les modèles pour l'implémentation de FLIN.
La différence est dans les détails. Rust n'exige le break avec valeur que dans loop (boucles infinies), pas dans for ou while. FLIN permet le break avec valeur dans n'importe quelle boucle, parce que le cas d'usage -- trouver une valeur dans une itération -- est tout aussi valide quel que soit le type de boucle.
FLIN rend aussi le type de résultat optionnel par défaut, reflétant la réalité qu'une boucle pourrait ne pas exécuter son instruction break. Rust réalise cela via son système de propriété et la garantie de la construction loop d'au moins une itération. FLIN adopte l'approche plus simple d'envelopper le résultat dans T?.
Ces sessions -- 152 et 153 -- représentent la maturation de FLIN en tant que langage. Le flux de contrôle de base (if, for, while, match) a été établi tôt. While-let et break avec valeur sont des raffinements qui rendent des patterns spécifiques plus propres sans ajouter de complexité fondamentale. Ce sont le genre de fonctionnalités qui font qu'un langage semble poli plutôt que simplement fonctionnel.
Ceci est la partie 43 de la série « How We Built FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et implémenté un langage de programmation à partir de zéro.
Navigation de la série : - [41] The Never Type and Exhaustiveness Checking - [42] Generic Bounds and Where Clauses - [43] While-Let Loops and Break With Value (vous êtes ici) - [44] Labeled Loops and Or-Patterns - [45] Advanced Type Features: The Complete Picture