Skip to content

CEP-0070: Value-Producing Loop Expressions

Draft

Draft proposal for loops that can produce a value through break payloads. V1 loops have success type void, and break has no payload.

Summary

Catalyst should eventually decide whether loop expressions can produce values, and whether that feature should use existing while / for loops or a dedicated endless loop { ... } form.

The promising direction is to introduce loop { ... } as an unconditional loop expression. A loop has no natural completion path, so any value it produces must come from a break payload. That keeps the rule simple: break value is to loop what return value is to a function.

Motivation

Expression-oriented code sometimes wants to compute a value by repeatedly doing work until a result is found:

const token = {
  var result: ?Token = null

  while not result {
    const next = scanner.next()

    if next.is_token() {
      result = next
    }
  }

  result.unwrap()
}

A value-producing loop can express this without sentinel state whose only purpose is to carry the result out of the loop.

Proposed Direction

Add a dedicated endless loop expression:

const token = loop {
  const next = scanner.next()

  if next.is_token() {
    break next
  }
}

The proposed break payload forms are:

  • break value: exit the nearest value-producing loop and make that loop produce value;
  • break :label value: exit the labeled value-producing loop and make that loop produce value, if labeled loop control is accepted.

A loop with no break payloads keeps success type void. A loop with one or more break payloads has a success type determined from the payload expressions that target that loop. The same compatibility model used for block and conditional expression values should apply: payloads must be compatible with the loop's expected type or with each other when no expected type is available.

Why loop

Using while or for as value-producing expressions makes natural completion part of the type rule. A finite for may exhaust its source, and a while condition may stop when the condition becomes false. If that exit path is reachable, the loop has no value to produce.

loop avoids that ambiguity because it has no condition and no iterator exhaustion edge. A loop either exits through break, diverges forever, or exits through another diverging expression such as return or error propagation.

This proposal should prefer loop unless a later design adds an explicit fallback result form for conditional or iterator loops.

Rejected or Deferred Shape

Value-producing while and for without a fallback are rejected by this proposal direction:

const found = for item in items {
  if matches(item) {
    break item
  }
}

The programmer should write an explicit local fallback:

var found: ?Item = null

for item in items {
  if matches(item) {
    found = item
    break
  }
}

A future proposal could add a fallback spelling for natural completion, such as a loop else branch, but that is separate from the core loop direction.

Cleanup and Ownership

For break value, the payload expression should be evaluated before exit cleanup begins, then transferred into the target loop's result slot using the same copy, move, borrow, and resource rules as an explicit block or return value. Cleanup must not dispose a value that has been moved into the loop result.

If labeled loop control is accepted, break :label value crosses all inner loops and blocks between the break site and the labeled loop. It must run pending defer statements, resource cleanup, and compiler-owned loop temporary cleanup in the same order as other early exits.

Existing CEP Overlap

This proposal owns break payloads, value-producing loop typing, and the possible loop keyword.

It does not own labeled control targets. Those are tracked by CEP-0069: Labeled Loop Control.

It also does not own value-producing local declarations. CEP-0068: Value-Producing Local Declarations asks whether a declaration statement can complete with a value. This proposal asks whether a loop can complete with a value supplied by break.

V1 Compatibility

V1 remains unchanged:

  • loop is not a loop form;
  • break does not accept a payload;
  • while and for have success type void;
  • code that needs a loop result uses an explicit local variable or extracts a helper function.

Accepting this proposal would reserve or contextualize loop and would relax programs that currently fail because they use break payloads. If loop becomes a full reserved keyword, migration diagnostics should identify existing bindings named loop.

Open Questions

  • Should loop be a reserved keyword or a contextual keyword before {?
  • Should break value be rejected in a loop whose expected result is void rather than silently discarding the payload?
  • Should a value-producing loop require an expected type, or can payloads infer a result type without context?
  • Should conditional or iterator loops ever gain a fallback result form, or should loop remain the only value-producing loop expression?
  • How should diagnostics explain a loop whose body has no reachable value break and no obvious diverging behavior?