Sessions 152 and 153 added two features that seem modest in scope but fundamentally change how iteration works in FLIN: while-let loops and break with value. Together, they bring Rust-inspired control flow to a language designed for application developers.
The motivation was practical. Before these features, consuming an iterator or processing a stream of optional values required awkward manual unwrapping and explicit loop control. After these features, the pattern-driven approach handles the unwrapping automatically, and loops can produce values just like match expressions and if-else chains.
While-Let: Pattern-Driven Iteration
A while-let loop continues iterating as long as a pattern matches:
items = [Some(1), Some(2), None, Some(4)]
index = 0while let Some(value) = items[index] { print(value) index++ } // Prints: 1, 2 // Stops at None because the pattern Some(value) does not match ```
The loop condition is not a boolean expression. It is a pattern match. On each iteration, the right-hand side is evaluated and matched against the pattern. If the pattern matches, the loop body executes with the pattern's bindings in scope. If the pattern does not match, the loop exits.
This is syntactic sugar for a common pattern:
// 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++ } ```
The while-let version is shorter, but more importantly, it is safer. The unwrap in the manual version is a potential source of runtime errors. The while-let version never unwraps -- it destructures, which is statically checked.
AST Representation
The while-let loop is represented as a new variant in the statement enum:
Stmt::WhileLet {
pattern: Pattern,
value: Expr,
body: Block,
span: Span,
}The pattern is the left side of the = in the condition. The value is the right side. The body is the loop body, executed on each iteration where the pattern matches.
Parser Implementation
The parser detects while-let by checking for the let keyword after while:
fn 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
The two-keyword sequence while let is unambiguous because let is not a valid expression start. The parser can distinguish between while condition { ... } and while let pattern = expr { ... } with a single token of lookahead.
Type Checking
The type checker validates that the pattern is compatible with the value's type:
fn check_while_let(&mut self, pattern: &Pattern, value: &Expr, body: &Block, span: Span) {
let value_type = self.infer_type(value);// The pattern determines what types are accepted // For enum variant patterns, check the variant exists match pattern { Pattern::EnumVariant { enum_name, variant, inner, .. } => { let enum_type = self.resolve_type(enum_name); self.validate_variant(enum_type, variant, inner);
// In the body, bind the inner pattern with the variant's data type 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(); } } } ```
The body is checked in a new scope where the pattern bindings are available. When the loop exits (because the pattern did not match), the bindings go out of scope.
Code Generation
The bytecode for a while-let loop combines pattern matching with loop control:
fn 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); } ```
The structure is: evaluate, check pattern, if no match jump to end, otherwise execute body and jump back to start. This is the standard loop-with-condition bytecode pattern, except the condition is a pattern match instead of a boolean test.
Break With Value
Session 153 added the ability for break to carry a value:
result = for item in items {
if item.matches(criteria) {
break item
}
}The break item exits the loop and produces item as the loop's value. The loop is an expression that evaluates to whatever value is passed to break.
This is directly inspired by Rust's break with value, and it eliminates a common pattern:
// 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 } } ```
The second version is shorter and communicates intent more clearly. The loop's purpose is to find a value, and break item makes that explicit.
AST Changes for Break With Value
The break statement gained an optional value:
Stmt::Break {
value: Option<Expr>,
label: Option<String>,
span: Span,
}When value is Some(expr), the break carries a value. When it is None, it is a plain break. The label field (discussed in the next article) allows breaking from outer loops.
Type Checking Break With Value
When a loop is used as an expression, the type checker must determine its result type. The result type comes from the break statements:
fn check_loop_as_expression(&mut self, loop_stmt: &Stmt) -> FlinType {
let break_types = self.collect_break_types(loop_stmt);if break_types.is_empty() { // No break-with-value: loop produces none return FlinType::Optional(Box::new(FlinType::Unknown)); }
// All break values must have compatible types let mut result_type = break_types[0].clone(); for bt in &break_types[1..] { result_type = self.unify(&result_type, bt); }
// The loop might not break (iterator might be empty) // So the result is optional FlinType::Optional(Box::new(result_type)) } ```
The result type is optional because the loop might execute zero iterations (if the collection is empty) or might not find a matching element. In either case, no break executes, and the loop produces none.
result: int? = for item in items {
if item > 10 {
break item
}
}if result { print("Found: " + text(result)) } else { print("No item > 10 found") } ```
Code Generation for Break With Value
fn emit_break_with_value(&mut self, value: &Option<Expr>) {
if let Some(expr) = value {
// Evaluate the value
self.emit_expr(expr);
// Store it in the loop's result slot
self.emit_store(self.current_loop_result_slot());
}
// Jump to the loop's exit point
self.emit_jump_to_loop_exit();
}Each loop that might produce a value allocates a result slot on the stack. When break value executes, the value is stored in that slot. After the loop exits, the result slot contains the loop's value (or none if no break executed).
While-Let with Break Value
While-let and break-with-value combine naturally:
// Process items until finding a specific condition
result = while let Some(item) = iterator.next() {
if item.is_special() {
break item.transform()
}
process(item)
}The while-let loop iterates as long as the iterator produces Some(item). Inside the loop, if a special item is found, the loop breaks with a transformed value. Otherwise, the item is processed and the loop continues.
Practical Patterns
Finding the First Match
first_admin = for user in users {
if user.role == "admin" {
break user
}
}Accumulating Until a Condition
total = 0
batch = while let Some(item) = queue.dequeue() {
total = total + item.value
if total > threshold {
break total
}
}Parsing with Optional Tokens
tokens = tokenize(input)
index = 0ast = while let Some(token) = tokens.get(index) { if token.kind == "EOF" { break ast_builder.finish() } ast_builder.consume(token) index++ } ```
Processing Paginated API Results
all_results: [User] = []
page = 1while 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++ } ```
Design Decisions
While-let patterns are limited to refutable patterns. Not every pattern makes sense in a while-let. Irrefutable patterns (like a simple identifier) would always match, creating an infinite loop. The compiler warns about irrefutable while-let patterns:
// 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
}Break value type must be consistent. If a loop has multiple break statements with values, all values must have compatible types:
result = 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)?The compiler unifies the break types. If they are different, the result is a union type, wrapped in optional.
Plain break still works. Adding break-with-value does not change the behavior of plain break. A break without a value exits the loop without producing a value (the result is none).
The Rust Parallel
These features are directly inspired by Rust. Rust's while let pattern, its break with value in loop blocks, and its loop-as-expression semantics were the models for FLIN's implementation.
The difference is in the details. Rust requires break with value only in loop (infinite loops), not in for or while. FLIN allows break with value in any loop, because the use case -- finding a value in an iteration -- is equally valid regardless of the loop kind.
FLIN also makes the result type optional by default, reflecting the reality that a loop might not execute its break statement. Rust achieves this through its ownership system and the loop construct's guarantee of at least one iteration. FLIN takes the simpler approach of wrapping the result in T?.
These sessions -- 152 and 153 -- represent FLIN's maturation as a language. The basic control flow (if, for, while, match) was established early. While-let and break-with-value are refinements that make specific patterns cleaner without adding fundamental complexity. They are the kind of features that make a language feel polished rather than merely functional.
---
This is Part 43 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and implemented a programming language from scratch.
Series Navigation: - [41] The Never Type and Exhaustiveness Checking - [42] Generic Bounds and Where Clauses - [43] While-Let Loops and Break With Value (you are here) - [44] Labeled Loops and Or-Patterns - [45] Advanced Type Features: The Complete Picture