Skip to content

Loops

Atoll's four for-loop forms, mutable iteration, value-bearing breaks, labels, and per-iteration cleanup.

Updated View as Markdown

for is the only loop keyword in Atoll. There is no while and no loop. The tokens between for and the body select one of four forms.

Form Shape Continues while
Iteration for pattern in value { ... } the iterable yields another value
Condition for condition { ... } the condition is true
Pattern consume for pattern := expression { ... } the expression matches the pattern
Infinite for { ... } a transfer expression leaves the loop

All four appear in one function here:

fn survey(values: []int): int {
    mut total := 0

    for value in values {
        total += value
    }

    mut countdown := 3
    for countdown > 0 {
        countdown -= 1
    }

    for index in 0..10 {
        total += index
    }

    for {
        break
    }

    return total
}

After for, an immediate { selects the infinite form; a top-level := selects pattern consumption; a top-level in selects iteration; anything else is a boolean condition. Delimiters inside the head do not affect that choice.

Iterate over values

for pattern in value evaluates the iterable once, then binds each yielded element to the pattern.

fn sum(values: []int): int {
    mut total := 0
    for value in values {
        total += value
    }
    return total
}

Ranges are iterable, both exclusive and inclusive:

fn triangle(n: int): int {
    mut total := 0
    for index in 0..n {
        total += index
    }
    for index in 0..=n {
        total += index
    }
    return total
}

A Map yields key/value pairs; the top-level tuple shorthand needs no parentheses:

fn weigh(): int {
    mut scores := Map.new[string, int]()
    scores.put("ada", 3)
    scores.put("grace", 5)

    mut total := 0
    for name, score in scores {
        total += score + name.len()
    }
    return total
}

Sets and string character iterators work the same way, and _ discards a yielded value you do not need:

fn f(text: string): int {
    mut unique := Set.new[int]()
    unique.add(3)

    mut total := 0
    for value in unique {
        total += value
    }
    for _ in text.chars() {
        total += 1
    }
    return total
}

Mutable iteration

for mut name in list binds each element mutably and writes the final value back to that position when the iteration ends.

fn double_all(): []int {
    mut values := [1, 2, 3]
    for mut value in values {
        value *= 2
    }
    return values
}

Two requirements are enforced. The iterable must be a mutable []T or [..]T binding:

fn f(): int {
    values := [1, 2, 3]
    for mut value in values {
        value *= 2
    }
    return values.len()
}

…and it must actually have elements to write back to, which a range does not:

fn f(): int {
    for mut index in 0..3 {
        index += 1
    }
    return 0
}

Write-back happens on normal completion of the iteration and on continue, after that iteration’s scope cleanup. A break, return, or propagated error leaves along its own transfer path. Do not use for mut when a partially transformed list would violate an invariant — build a new list instead:

fn doubled(values: []int): []int {
    mut out: []int = []
    for value in values {
        out.add(value * 2)
    }
    return out
}

Loop on a condition

for condition re-evaluates a bool before every iteration. If the first check is false the body never runs.

fn countdown(start: int): int {
    mut remaining := start
    mut ticks := 0
    for remaining > 0 {
        ticks += 1
        remaining -= 1
    }
    return ticks
}

The condition is an ordinary expression, so it can call methods and combine several tests:

fn advance_until(values: []int, budget: int): int {
    mut index: u32 = 0
    mut spent := 0
    for values.is_not_empty() && spent < budget && index < values.len().to_u32() {
        spent += values[index] ?? 0
        index += 1
    }
    return spent
}

Consume matching values

for pattern := expression re-evaluates the expression at every loop head. A match runs one iteration with the pattern’s bindings; a failure ends the loop.

struct Inbox { remaining: int }

impl Inbox {
    fn next(mut self): int? {
        if self.remaining <= 0 { return None }
        self.remaining -= 1
        return Some(self.remaining)
    }
}

fn drain_inbox(): int {
    mut inbox := Inbox { remaining: 3 }
    mut total := 0
    for Some(message) := inbox.next() {
        total += message
    }
    return total
}

This is repeated pattern matching where the non-matching case exits, which makes it the natural shape for Option-returning sources: queues, cursors, hand-written iterators, and protocol readers. Bindings are fresh each iteration and leave scope when that iteration finishes, including on continue.

Loop indefinitely

A subjectless for repeats until the body transfers control out.

enum Message { Data(int), Shutdown }

struct Channel { pending: int }

impl Channel {
    fn receive(mut self): Message {
        if self.pending <= 0 { return Shutdown }
        self.pending -= 1
        return Data(self.pending)
    }
}

fn pump(): int {
    mut channel := Channel { pending: 4 }
    mut handled := 0
    for {
        message := channel.receive()
        match message {
            Shutdown => break
            Data(value) => handled += value
        }
    }
    return handled
}

Break with a value

break value makes the loop produce a result. Use the bare for { } form for this: it has no ordinary exhaustion path, so a value-bearing break is the only way out and the loop’s type is that value’s type.

fn first_large(values: []int): int {
    mut index: u32 = 0
    answer := for {
        if index >= values.len().to_u32() {
            break -1
        }
        value := values[index] ?? 0
        if value > 10 {
            break value
        }
        index += 1
    }
    return answer
}

An iteration, condition, or pattern-consume loop can also finish through its head, and that path contributes void. Assigning such a loop to T is therefore rejected:

fn f(values: []int): int {
    mut index := 0
    result: int = for index < values.len() {
        index += 1
        break 3
    }
    return result
}

When exhaustion is meaningful, return an Option or keep an explicit result binding rather than inventing a sentinel:

fn first_large(values: []int): int? {
    for value in values {
        if value > 10 {
            return Some(value)
        }
    }
    return None
}

Continue

continue abandons the rest of the body and resumes at the loop head: an iteration loop advances the iterator, a condition loop rechecks its condition, and a pattern-consume loop re-evaluates and re-matches.

fn sum_positive(values: []int): int {
    mut total := 0
    for value in values {
        if value < 0 {
            continue
        }
        if value == 0 {
            continue
        }
        total += value
    }
    return total
}

Labels

Prefix a loop with 'name: and pass the label to break or continue when the target would otherwise be the innermost loop.

fn scan(rows: [][]int): int {
    mut visited := 0
    'outer: for row in rows {
        for cell in row {
            if cell == 0 {
                break 'outer
            }
            visited += 1
        }
    }
    return visited
}

continue 'label restarts the labeled loop, leaving every inner scope first:

fn count_clean_rows(rows: [][]int): int {
    mut clean := 0
    'rows: for row in rows {
        for cell in row {
            if cell < 0 {
                continue 'rows
            }
        }
        clean += 1
    }
    return clean
}

A labeled break can carry a value out of a labeled bare loop:

fn find_over(groups: [][]int, threshold: int): int {
    mut group_index: u32 = 0
    found := 'search: for {
        if group_index >= groups.len().to_u32() {
            break 'search -1
        }
        group := groups[group_index] ?? []
        for item in group {
            if item > threshold {
                break 'search item
            }
        }
        group_index += 1
    }
    return found
}

The label must name an enclosing loop. Both of these are rejected:

fn f(): int {
    break
    return 0
}
fn f(values: []int): int {
    for value in values {
        break 'nope
    }
    return 0
}

Per-iteration cleanup

The loop body is a lexical scope. Values and defers created in one iteration are cleaned up before the next iteration starts — including on continue.

error IoError { Denied }

struct File { id: int }

impl File {
    fn open(name: string): File ! IoError {
        if name.is_empty() { error Denied }
        return File { id: name.len() }
    }
    fn read(self): string ! IoError { return "line" }
    fn close(self): void { println("closed ${self.id}") }
}

fn total_size(paths: []string): int ! IoError {
    mut total := 0
    for path in paths {
        file := File.open(path)?
        defer file.close()

        body := file.read()?
        if body.is_empty() {
            continue
        }
        total += body.len()
    }
    return total
}

Each iteration closes its own file. A defer written outside the loop would instead wait for the surrounding scope. A labeled break can leave several nested loop scopes at once; cleanup runs innermost-first before control reaches the labeled target.

Putting it together

error ImportError { Malformed { line: int } }

struct Record { key: string, weight: int }

fn parse_record(raw: string, line: int): Record ! ImportError {
    if raw.is_empty() { error Malformed { line: line } }
    return Record { key: raw, weight: raw.len() }
}

fn import_rows(rows: [][]string): Map[string, int] ! ImportError {
    mut totals := Map.new[string, int]()
    mut line := 0

    'rows: for row in rows {
        line += 1
        mut row_weight := 0

        for raw in row {
            if raw.is_empty() {
                continue
            }
            if raw == "SKIP-ROW" {
                continue 'rows
            }
            if raw == "STOP" {
                break 'rows
            }
            record := parse_record(raw, line)?
            row_weight += record.weight
            totals.put(record.key, record.weight)
        }

        totals.put("row-${line}", row_weight)
    }

    return totals
}

continue skips one cell, continue 'rows abandons the rest of a row, break 'rows ends the import, and ? leaves the function entirely with an ImportError. See Transfers for the cleanup sequence each of those shares.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close