Les sessions 154 et 155 ont abordé deux fonctionnalités qui répondent à une plainte courante sur les langages de programmation : l'impossibilité d'exprimer certains patterns de flux de contrôle proprement. Les boucles étiquetées résolvent le problème d'échappement des boucles imbriquées. Les motifs Or résolvent le problème de la duplication des bras de match. Ce sont deux petites fonctionnalités avec un impact ergonomique disproportionné.
Le problème des boucles imbriquées
Considérons la recherche d'une valeur dans une grille 2D :
flin// Without labeled loops
found = false
for row in grid {
for cell in row {
if cell == target {
found = true
break // breaks inner loop only
}
}
if found { break } // need this extra check
}Le break à l'intérieur de la boucle interne ne sort que de la boucle interne. Pour sortir de la boucle externe, on a besoin d'une variable drapeau et d'une vérification supplémentaire. C'est verbeux, sujet aux erreurs (oubliez la vérification externe et vous avez un bug), et obscurcit l'intention.
Avec les boucles étiquetées :
flin'search: for row in grid {
for cell in row {
if cell == target {
break 'search // exits the outer loop directly
}
}
}Une seule instruction. Intention claire. Pas de variable drapeau. Pas de vérification supplémentaire.
Syntaxe des étiquettes
FLIN utilise la syntaxe préfixée par un apostrophe pour les étiquettes, suivant la convention de Rust :
flin'outer: for i in 0..10 {
'inner: for j in 0..10 {
if condition(i, j) {
break 'outer // exit both loops
}
if other(j) {
continue 'outer // skip to next i iteration
}
}
}Le préfixe apostrophe ('label) a été choisi par rapport aux alternatives pour plusieurs raisons :
- Les deux-points seuls (
outer:) pourraient être confondus avec les annotations de type - Les arobase (
@outer) sont déjà utilisés pour les opérations temporelles dans FLIN - Le préfixe apostrophe est familier grâce à Rust et visuellement distinct des autres syntaxes
Les étiquettes ont une portée limitée à la boucle qu'elles annotent. Utiliser une étiquette qui ne fait pas référence à une boucle englobante est une erreur de compilation :
flin'outer: for i in items {
// ...
}
// ERROR: 'outer' is not an enclosing loop
break 'outerReprésentation AST
Les étiquettes sont ajoutées aux instructions de boucle et aux instructions break/continue :
rustStmt::For {
label: Option<String>, // NEW
variable: String,
iterator: Expr,
body: Block,
span: Span,
}
Stmt::While {
label: Option<String>, // NEW
condition: Expr,
body: Block,
span: Span,
}
Stmt::Break {
value: Option<Expr>,
label: Option<String>, // NEW
span: Span,
}
Stmt::Continue {
label: Option<String>, // NEW
span: Span,
}Le champ label est Option<String> -- la plupart des boucles et des breaks n'utilisent pas d'étiquettes, donc le cas courant ne comporte aucune surcharge.
Modifications de l'analyseur syntaxique
L'analyseur détecte les étiquettes en vérifiant le motif apostrophe-identifiant avant un mot-clé de boucle :
rustfn parse_statement(&mut self) -> Result<Stmt, ParseError> {
// Check for label: 'name: for/while
if self.check(&Token::Tick) {
let label = self.parse_label()?;
self.expect(&Token::Colon)?;
if self.check_keyword("for") {
return self.parse_for_with_label(Some(label));
}
if self.check_keyword("while") {
return self.parse_while_with_label(Some(label));
}
return Err(ParseError::new(
"label must be followed by 'for' or 'while'",
self.current_span(),
));
}
// ... rest of statement parsing
}
fn parse_label(&mut self) -> Result<String, ParseError> {
self.expect(&Token::Tick)?;
self.expect_identifier()
}Break et continue analysent une étiquette optionnelle :
rustfn parse_break(&mut self) -> Result<Stmt, ParseError> {
self.expect_keyword("break")?;
let label = if self.check(&Token::Tick) {
Some(self.parse_label()?)
} else {
None
};
let value = if !self.is_at_statement_end() && label.is_none() {
Some(self.parse_expression()?)
} else {
None
};
Ok(Stmt::Break { value, label, span: self.current_span() })
}Notez la priorité : break 'label est vérifié avant break value. Si un apostrophe suit break, c'est une étiquette, pas une expression de valeur. Si aucun n'est présent, c'est un break simple.
Vérification de types des étiquettes
Le vérificateur de types maintient une pile d'étiquettes de boucles actives :
ruststruct LoopContext {
label: Option<String>,
break_types: Vec<FlinType>,
}
fn check_for_loop(&mut self, label: &Option<String>, variable: &str, iter: &Expr, body: &Block) {
self.loop_stack.push(LoopContext {
label: label.clone(),
break_types: vec![],
});
// ... check body ...
self.loop_stack.pop();
}
fn check_break(&mut self, label: &Option<String>, value: &Option<Expr>, span: Span) {
let target = match label {
Some(name) => {
self.loop_stack.iter().rev()
.find(|ctx| ctx.label.as_ref() == Some(name))
}
None => self.loop_stack.last(),
};
if target.is_none() {
if let Some(name) = label {
self.report_error(&format!("no enclosing loop with label '{}'", name));
} else {
self.report_error("break outside of loop");
}
}
}Quand un break étiqueté est rencontré, le vérificateur de types cherche dans la pile de boucles une étiquette correspondante. Si aucune n'est trouvée, il signale une erreur. Cela empêche de sortir vers une boucle qui n'existe pas ou qui n'est pas un ancêtre de la position courante.
Génération de code pour les breaks étiquetés
Au niveau du bytecode, les breaks étiquetés se compilent en sauts qui ciblent le point de sortie de la boucle spécifique :
rustfn emit_for_loop(&mut self, label: &Option<String>, var: &str, iter: &Expr, body: &Block) {
let loop_info = LoopInfo {
label: label.clone(),
start_offset: self.current_offset(),
break_jumps: vec![],
continue_jumps: vec![],
};
self.loop_info_stack.push(loop_info);
// ... emit loop body ...
let exit_offset = self.current_offset();
let loop_info = self.loop_info_stack.pop().unwrap();
for jump in loop_info.break_jumps {
self.patch_jump_to(jump, exit_offset);
}
}
fn emit_break(&mut self, label: &Option<String>, value: &Option<Expr>) {
if let Some(value) = value {
self.emit_expr(value);
self.emit_store(self.get_loop_result_slot(label));
}
let jump = self.emit_jump_forward();
let target = match label {
Some(name) => self.loop_info_stack.iter_mut().rev()
.find(|info| info.label.as_ref() == Some(name)),
None => self.loop_info_stack.last_mut(),
};
if let Some(target) = target {
target.break_jumps.push(jump);
}
}Quand break 'outer est émis, le saut est enregistré dans la liste de sauts de break de la boucle externe. Quand la boucle externe finit d'émettre, elle patche tous ses sauts de break vers le point de sortie. Cela gère correctement le cas où un break dans une boucle interne saute au-delà de la sortie de la boucle interne vers la sortie de la boucle externe.
Continue étiqueté
L'instruction continue supporte aussi les étiquettes :
flin'outer: for row in grid {
for cell in row {
if cell.is_empty() {
continue 'outer // skip to next row
}
process(cell)
}
}continue 'outer saute le reste de l'itération courante de la boucle interne et le reste de l'itération courante de la boucle externe, sautant au début de la prochaine itération externe. Sans étiquettes, il faudrait une variable drapeau pour obtenir le même effet.
Motifs Or
Les sessions 154-155 ont aussi ajouté les motifs Or aux expressions match. Un motif Or combine plusieurs motifs en un seul bras :
flinmatch status {
"active" | "enabled" -> show_active()
"pending" | "queued" | "waiting" -> show_pending()
"error" | "failed" -> show_error()
_ -> show_unknown()
}Le | entre les motifs signifie « correspondre si l'un de ces motifs correspond ». Le corps du bras s'exécute une fois, quelle que soit l'alternative qui a correspondu.
AST des motifs Or
Les motifs Or sont représentés comme un nouveau variant de motif :
rustpub enum Pattern {
// ... existing variants ...
Or(Vec<Pattern>),
}Chaque élément du vecteur est un motif alternatif. Le motif Or correspond si une alternative quelconque correspond.
Analyse syntaxique des motifs Or
L'analyseur collecte les alternatives séparées par | dans un bras de match :
rustfn parse_match_arm_pattern(&mut self) -> Result<Pattern, ParseError> {
let first = self.parse_single_pattern()?;
if self.check(&Token::Pipe) {
let mut alternatives = vec![first];
while self.match_token(&Token::Pipe) {
alternatives.push(self.parse_single_pattern()?);
}
Ok(Pattern::Or(alternatives))
} else {
Ok(first)
}
}Notez que | en contexte de motif signifie « motif Or », tandis que | en contexte de type signifie « type union », et |> signifie « pipeline ». L'analyseur désambiguïse par le contexte : à l'intérieur d'un bras de match avant ->, c'est un motif. Dans une annotation de type, c'est une union. Après une expression de valeur, c'est un pipeline.
Vérification de types des motifs Or
Le vérificateur de types valide que toutes les alternatives d'un motif Or lient les mêmes variables avec des types compatibles :
rustfn check_or_pattern(&mut self, patterns: &[Pattern], value_type: &FlinType) {
if patterns.is_empty() { return; }
let first_bindings = self.collect_pattern_bindings(&patterns[0], value_type);
for (i, pattern) in patterns.iter().enumerate().skip(1) {
let bindings = self.collect_pattern_bindings(pattern, value_type);
for (name, flin_type) in &first_bindings {
match bindings.get(name) {
Some(other_type) => {
if !self.types_compatible(flin_type, other_type) {
self.report_error(&format!(
"or-pattern alternative {} binds '{}' as {} but alternative 1 binds it as {}",
i + 1, name, other_type.display_name(), flin_type.display_name()
));
}
}
None => {
self.report_error(&format!(
"or-pattern alternative {} does not bind '{}'",
i + 1, name
));
}
}
}
}
}Cette validation garantit que le corps du bras peut utiliser n'importe quelle variable liée quelle que soit l'alternative qui a correspondu. Si Circle(r) | Square(r) est un motif Or, les deux alternatives doivent lier r, et les deux doivent le lier avec des types compatibles.
Motifs Or avec des variants d'enum
Les motifs Or se combinent naturellement avec les unions étiquetées :
flinenum Shape {
Circle(number),
Square(number),
Rectangle(number, number),
Triangle(number, number, number),
Point
}
match shape {
Circle(size) | Square(size) -> {
print("Simple shape with size: " + text(size))
}
Rectangle(w, h) -> {
print("Rectangle: " + text(w) + " x " + text(h))
}
Triangle(a, b, c) -> {
print("Triangle with sides: " + text(a) + ", " + text(b) + ", " + text(c))
}
Point -> print("Point")
}Le motif Circle(size) | Square(size) correspond à l'un ou l'autre variant et lie la valeur interne à size. Le corps du bras n'a pas besoin de savoir quel variant a correspondu -- il ne s'intéresse qu'à la taille.
Motifs Or et exhaustivité
Les motifs Or participent à la vérification d'exhaustivité. Chaque alternative dans un motif Or couvre son variant respectif :
flinenum Result<T, E> {
Ok(T),
Err(E),
Pending
}
match result {
Ok(v) | Pending -> handle_optimistic(v) // ERROR: Pending does not bind v
}Cette erreur souligne une contrainte importante : si une alternative lie une variable, toutes doivent le faire. On ne peut pas utiliser Ok(v) | Pending parce que Pending ne porte pas de données et ne peut pas lier v.
Le motif correct :
flinmatch result {
Ok(v) -> handle_success(v)
Err(e) -> handle_error(e)
Pending -> handle_pending()
}Ou, si le corps n'a pas besoin de la valeur liée :
flinmatch result {
Ok(_) | Pending -> print("not an error")
Err(e) -> handle_error(e)
}Les jokers (_) ne créent pas de liaisons, donc Ok(_) | Pending est valide -- aucune alternative ne lie de variables.
Génération de code pour les motifs Or
Les motifs Or se compilent en une série de vérifications de motifs avec un corps partagé :
rustfn emit_or_pattern_check(&mut self, patterns: &[Pattern]) -> usize {
let mut success_jumps = vec![];
for (i, pattern) in patterns.iter().enumerate() {
let match_result = self.emit_pattern_check(pattern);
if i < patterns.len() - 1 {
success_jumps.push(self.emit_jump_if_true());
}
}
let skip_jump = self.emit_jump_if_false();
for jump in success_jumps {
self.patch_jump(jump);
}
skip_jump
}Chaque alternative est vérifiée en séquence. Si l'une correspond, l'exécution tombe dans le corps du bras. Ce n'est que si toutes les alternatives échouent que le corps du bras est sauté.
Combiner les deux fonctionnalités
Les boucles étiquetées et les motifs Or peuvent apparaître dans le même code :
flin'search: for row in grid {
for cell in row {
match cell.status {
"found" | "confirmed" -> {
result = cell
break 'search
}
"blocked" | "invalid" -> continue
_ -> process(cell)
}
}
}Le motif Or "found" | "confirmed" combine deux valeurs de statut en un seul bras. Le break étiqueté break 'search sort de la boucle externe quand une correspondance est trouvée. Le motif Or "blocked" | "invalid" saute le traitement pour ces statuts. Trois fonctionnalités -- motifs Or, boucles étiquetées et break avec valeur -- travaillant ensemble.
Statistiques d'implémentation
Les sessions 154-155 ont modifié les fichiers suivants :
| Fichier | Modifications |
|---|---|
src/parser/ast.rs | Champs label sur les boucles, break, continue ; Pattern::Or |
src/parser/parser.rs | Analyse des étiquettes, analyse des motifs Or |
src/typechecker/checker.rs | Pile d'étiquettes de boucle, validation des liaisons des motifs Or |
src/codegen/emitter.rs | Résolution des sauts étiquetés, génération de code des motifs Or |
src/fmt/formatter.rs | Formatage des étiquettes et des motifs Or |
tests/integration_e2e.rs | Nouveaux tests pour les deux fonctionnalités |
Les deux fonctionnalités étaient additives -- aucun test existant n'a été modifié, et tous les tests existants ont continué de passer.
Pourquoi ces fonctionnalités ensemble
Les boucles étiquetées et les motifs Or peuvent sembler sans rapport, mais ils partagent un objectif commun : réduire l'écart entre ce que le développeur veut exprimer et ce que la syntaxe du langage lui permet d'exprimer.
Sans boucles étiquetées, le développeur veut dire « sortir des deux boucles » mais doit utiliser une variable drapeau. Sans motifs Or, le développeur veut dire « traiter ces cas de la même façon » mais doit dupliquer le corps du bras. Les deux fonctionnalités comblent un fossé d'expressivité.
La philosophie de développement de FLIN est d'identifier ces fossés et de les combler avec des fonctionnalités minimales et composables. Les boucles étiquetées ajoutent une étiquette aux boucles existantes et une cible aux breaks existants. Les motifs Or ajoutent un | aux motifs existants. Aucun ne change la sémantique du langage -- les deux sont réductibles à des constructions existantes. Mais les deux améliorent considérablement l'expérience développeur.
Ceci est la partie 44 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 : - [42] Generic Bounds and Where Clauses - [43] While-Let Loops and Break With Value - [44] Labeled Loops and Or-Patterns (vous êtes ici) - [45] Advanced Type Features: The Complete Picture