From 06b8c4288748145cfe057fb90b0341dc1cee2810 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 17:57:32 +0100 Subject: [PATCH] =?UTF-8?q?fix(analyser):=20give=20a=20while=20true=20loop?= =?UTF-8?q?=20no=20break=20leaves=20no=20value=20=F0=9F=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- manual/src/reference/while-loop.md | 15 ++++++++ ndc_analyser/src/analyser/declarations.rs | 4 +-- ndc_analyser/src/analyser/mod.rs | 34 ++++++++++++------- ...078_an_endless_while_loop_has_no_value.ndc | 12 +++++++ ..._while_true_loop_left_by_break_is_unit.ndc | 10 ++++++ 5 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 tests/functional/programs/004_basic/078_an_endless_while_loop_has_no_value.ndc create mode 100644 tests/functional/programs/004_basic/079_a_while_true_loop_left_by_break_is_unit.ndc diff --git a/manual/src/reference/while-loop.md b/manual/src/reference/while-loop.md index 0984c591..16a6a0aa 100644 --- a/manual/src/reference/while-loop.md +++ b/manual/src/reference/while-loop.md @@ -20,3 +20,18 @@ while n <= 100 { n += 1; } ``` + +A `while true` loop that no `break` leaves never ends, so it has no value at all, like a `return`. A function can end in one and leave it with `return`, and it returns what its `return`s do: + +```ndc +fn first_square_above(n) { + let i = 1; + while true { + if i * i > n { + return i * i; + } + i += 1; + } +} +print(first_square_above(50) + 1); // 65 +``` diff --git a/ndc_analyser/src/analyser/declarations.rs b/ndc_analyser/src/analyser/declarations.rs index 77be885b..f37144b6 100644 --- a/ndc_analyser/src/analyser/declarations.rs +++ b/ndc_analyser/src/analyser/declarations.rs @@ -115,7 +115,7 @@ impl Analyser { }); // A function body starts outside of any loop: `break` can't // leave the function to end a loop around its declaration. - let outer_loop_depth = std::mem::take(&mut self.loop_depth); + let outer_loops = std::mem::take(&mut self.loops); for (param, typ) in parameters.iter().zip(¶m_types) { let symbol = self.resolver.declared_symbol(param.lvalue.id); self.types.declare(symbol, typ.clone()); @@ -127,7 +127,7 @@ impl Analyser { let implicit_return = self.analyse_expr(body); let mut returns = self.return_type_stack.pop().unwrap(); - self.loop_depth = outer_loop_depth; + self.loops = outer_loops; // The body's value is returned like the values of `return`. An // annotated return type is what each of them returns, which can diff --git a/ndc_analyser/src/analyser/mod.rs b/ndc_analyser/src/analyser/mod.rs index 0f75087f..ddb59a70 100644 --- a/ndc_analyser/src/analyser/mod.rs +++ b/ndc_analyser/src/analyser/mod.rs @@ -113,9 +113,11 @@ pub struct Analyser { /// What each enclosing function returns with `return`, innermost last. /// Pushed on function entry, popped on exit. return_type_stack: Vec, - /// How many loops enclose the expression being analysed in the current - /// function, so `break` and `continue` outside of one are reported. - loop_depth: usize, + /// The loops that enclose the expression being analysed in the current + /// function, innermost last, each with whether a `break` leaves it: a + /// `while true` that none leaves never ends. `break` and `continue` + /// outside of one are reported. + loops: Vec, /// What the current batch's analysis has found so far. result: AnalysisResult, /// The symbols the current batch's uses pick, which the layout turns into @@ -173,7 +175,7 @@ impl Analyser { struct_registry: StructRegistry::default(), struct_types: AHashMap::new(), return_type_stack: Vec::new(), - loop_depth: 0, + loops: Vec::new(), result: AnalysisResult::default(), picks: Picks::default(), laid_out_scopes: 0, @@ -411,13 +413,14 @@ impl Analyser { | NumericLiteral::Complex(_), ) => StaticType::Number, Expression::Break => { - if self.loop_depth == 0 { - self.emit(AnalysisError::break_outside_loop(*span)); + match self.loops.last_mut() { + Some(broken) => *broken = true, + None => self.emit(AnalysisError::break_outside_loop(*span)), } StaticType::Never } Expression::Continue => { - if self.loop_depth == 0 { + if self.loops.is_empty() { self.emit(AnalysisError::continue_outside_loop(*span)); } StaticType::Never @@ -533,11 +536,18 @@ impl Analyser { } => { // The condition is part of the loop too, as it runs on every // iteration. - self.loop_depth += 1; + self.loops.push(false); self.analyse_expr(expression); self.analyse_expr(loop_body); - self.loop_depth -= 1; - StaticType::unit() + let broken = self.loops.pop().expect("the loop was pushed"); + // A `while true` that no `break` leaves only ends by + // returning, so it has no value, like a `return`: a function + // whose body ends in one returns what its `return`s do. + if !broken && matches!(expression.expression, Expression::BoolLiteral(true)) { + StaticType::Never + } else { + StaticType::unit() + } } Expression::For { iterations, body } => { // A map comprehension's default is evaluated once, before the @@ -700,7 +710,7 @@ impl Analyser { let found_type = self.constrain_iteration(sequence_type, sequence_span); - self.loop_depth += 1; + self.loops.push(false); // A `for` binder has no type annotation. self.declare_binding_pattern(l_value, None, found_type, sequence_span); } @@ -734,7 +744,7 @@ impl Analyser { } }; if matches!(iteration, ForIteration::Iteration { .. }) { - self.loop_depth -= 1; + self.loops.pop(); } typ } diff --git a/tests/functional/programs/004_basic/078_an_endless_while_loop_has_no_value.ndc b/tests/functional/programs/004_basic/078_an_endless_while_loop_has_no_value.ndc new file mode 100644 index 00000000..08d76f2e --- /dev/null +++ b/tests/functional/programs/004_basic/078_an_endless_while_loop_has_no_value.ndc @@ -0,0 +1,12 @@ +// A `while true` that no `break` leaves never ends, so a function ending in +// one returns what its `return`s do: a list of pairs, whose sum is typed. +// expect-output: 4 +fn grow(pairs) { + while true { + if pairs.len > 2 { + return pairs; + } + pairs.push((1, 1)); + } +} +print([hi - lo + 1 for (lo, hi) in grow([(1, 2)])].sum); diff --git a/tests/functional/programs/004_basic/079_a_while_true_loop_left_by_break_is_unit.ndc b/tests/functional/programs/004_basic/079_a_while_true_loop_left_by_break_is_unit.ndc new file mode 100644 index 00000000..2211cd3e --- /dev/null +++ b/tests/functional/programs/004_basic/079_a_while_true_loop_left_by_break_is_unit.ndc @@ -0,0 +1,10 @@ +// A `while true` that a `break` leaves ends normally, with the unit value. +// expect-output: true +let i = 0; +let value = while true { + i += 1; + if i > 3 { + break; + } +}; +print(value == ());