Documentation / Loops

Loops

Loops repeat work without hiding which kind of repetition runs. Moth uses one loop keyword for conditions, collections and numeric ranges. The header tells you which form you face.

Each section starts with a practical explanation. Switch only that section to Advanced when you need the exact rules and edge cases.

Choose the explanation level: Conditional loops

Conditional loops

A conditional loop keeps repeating while a condition is true.

The condition is checked before each trip through the loop. This means the body might not run at all.


    count ~= 0

    loop count < 3:
        io.line([: Count: [count]])
        count += 1
    ;
    

This prints three lines. Once count < 3 becomes false, the loop ends and the program continues after it.

Make sure the condition can change

A condition that never becomes false keeps the loop running forever.


    running ~= true

    loop running:
        do_some_work()

        if work_is_finished():
            running = false
        ;
    ;
    

Use a conditional loop when repetition depends on a changing state rather than a collection or a known numeric range.

Moth uses one loop keyword for conditional loops, collection iteration and numeric ranges.

A conditional loop evaluates a Bool condition before each iteration and runs its body while that condition remains true.


    count ~= 0

    loop count < 3:
        io.line([: [count]])
        count += 1
    ;
    

Contract

  • The form is loop condition: ... ;.
  • The condition must resolve to Bool.
  • The condition is checked before the first iteration, so the body may run zero times.
  • It is checked again after each completed iteration.
  • Conditional loops do not declare item or index bindings.
  • There is no separate while keyword.
  • break exits the nearest active loop.
  • continue skips the rest of the current body and starts the next condition check.

Header classification

A top-level to marker makes the header a range loop.

Without a range marker, an explicit |...| suffix makes the header a collection loop. Without bindings, a collection-typed header is still a collection loop. Every other header must type-check as a Bool condition.

Scope

The body is a child scope. Names declared inside it are not visible after the loop.

Choose the explanation level: Collection loops

Collection loops

A collection loop visits each value in a collection, from the first item to the last.


    names = {"Priya", "Grace", "Linus"}

    loop names |name|:
        io.line([: Hello, [name]])
    ;
    

The name inside |...| is created for the loop body. On each iteration it refers to the current item.

Add an index when you need one

A second name receives the zero-based position of the item.


    names = {"Priya", "Grace", "Linus"}

    loop names |name, index|:
        io.line([: [index]: [name]])
    ;
    

The first item has index 0.

Ignore the item when only the repetition matters

Bindings are optional.


    items = {10, 20, 30}
    count ~= 0

    loop items:
        count += 1
    ;
    

After this loop, count is 3.

A collection loop visits the values in a Moth collection in collection order.


    names = {"Priya", "Grace", "Linus"}

    loop names |name, index|:
        io.line([: [index]: [name]])
    ;
    

Forms


    loop items:
        count += 1
    ;

    loop items |item|:
        inspect(item)
    ;

    loop items |item, index|:
        inspect(item)
    ;
    

Contract

  • The source expression must have a collection type.
  • Collection loops do not use a general iterable protocol.
  • Hash maps are not collection-loop sources.
  • Bindings are optional.
  • The first binding receives the current item and has the collection's element type.
  • The optional second binding receives the zero-based iteration index and has type Int.
  • Binding names are written inside one |...| suffix.
  • A binding list may contain one or two names.
  • An empty binding list, a third binding or duplicate binding names are invalid.
  • Loop bindings are immutable body-local names.
  • A binding name must not collide with another visible name.
  • An empty collection runs the body zero times.

Evaluation

The source expression is evaluated once before iteration begins. The collection length is captured at the same point.

The loop then visits zero-based indexes from 0 up to, but not including, that captured length.

Removed syntax

The old in form is not valid.


    loop item in items:
        inspect(item)
    ;
    

Write the collection source first, then place bindings after it:


    loop items |item|:
        inspect(item)
    ;
    
Choose the explanation level: Range loops

Range loops

A range loop counts through numbers.

to stops before the end value:


    loop 0 to 5 |number|:
        io.line([: [number]])
    ;
    

This produces 0, 1, 2, 3 and 4.

Use to & when the end value should be included:


    loop 0 to & 5 |number|:
        io.line([: [number]])
    ;
    

This also produces 5.

Start from zero

You can leave out the first 0.


    loop to 5 |number|:
        io.line([: [number]])
    ;
    

Change the step

Use by to skip between values:


    loop 0 to 10 by 2 |number|:
        io.line([: [number]])
    ;
    

This produces 0, 2, 4, 6 and 8.

Count downward

The direction comes from the bounds. You do not need a reverse keyword.


    loop 5 to 0 |number|:
        io.line([: [number]])
    ;
    

Float ranges need an explicit step:


    loop 0.0 to 1.0 by 0.1 |amount|:
        io.line([: [amount]])
    ;
    

A range loop generates numeric values directly from its loop header.


    loop 0 to 10 by 2 |value, index|:
        io.line([: [index]: [value]])
    ;
    

Forms


        loop start to end:
            ...

        loop start to & end |value|:
            ...

        loop start to end by step |value, index|:
            ...

        loop to end |value|:
            ...

        loop to & end |value|:
            ...
    

Bounds

  • to uses an exclusive end bound.
  • to & uses an inclusive end bound.
  • loop to end starts at 0.
  • loop to & end starts at 0 and includes the end value.
  • Ascending bounds count upward.
  • Descending bounds count downward.
  • There is no reverse keyword.
  • When start and end are equal, an exclusive range has no iterations and an inclusive range has one.

Step

  • Without by, integer ranges use a step magnitude of 1.
  • by supplies a magnitude. The compiler applies the direction implied by the bounds.
  • A literal zero step is rejected during compilation.
  • A non-literal step is checked before the first iteration. A value of zero fails at runtime.
  • Any range using Float requires an explicit by step.
  • Start, end and step expressions must have numeric types.

    loop 0.0 to 1.0 by 0.1 |amount|:
        io.line([: [amount]])
    ;
    

Evaluation

Start, end and explicit step expressions are evaluated once, from left to right, before the first bounds check.

The loop keeps those evaluated values for the complete iteration.

Bindings

Bindings are optional.

The first binding receives the current range value. It has type Float when any range operand is Float. Otherwise it has type Int.

The optional second binding receives the zero-based iteration index and always has type Int.

Bindings use the same one-or-two-name |...| rules as collection loops.

Checked updates

Runtime range counter updates use checked numeric operations. Their failures follow the numeric failure mode of the enclosing function or entry-selected start code.

Const range loops diagnose statically known counter failures while folding.

Choose the explanation level: Loop control

Loop control

Use break when a loop should end early.

Use continue when the current iteration should stop but the loop itself should keep going.


    loop 0 to 10 |number|:
        if number is 2:
            continue
        ;

        if number is 7:
            break
        ;

        io.line([: [number]])
    ;
    

The loop skips 2. It ends when it reaches 7, so 7 and the later numbers are not printed.

Nested loops

break and continue affect the closest loop around them.


    loop 0 to 3 |row|:
        loop 0 to 3 |column|:
            if column is 1:
                continue
            ;

            io.line([: row [row], column [column]])
        ;
    ;
    

In this example, continue skips one column. The outer row loop keeps running.

break and continue are statement-only controls for the nearest active loop.


    loop 0 to 10 |number|:
        if number is 2:
            continue
        ;

        if number is 7:
            break
        ;

        io.line([: [number]])
    ;
    

break

break exits the nearest active loop immediately. Statements later in that loop body do not run for the current iteration.

continue

continue skips the rest of the nearest active loop body.

The next action depends on the loop form:

  • a conditional loop checks its condition again
  • a collection loop advances to its next item
  • a range loop advances its counter, then checks the next bound

Rules

  • break and continue are valid only inside a loop.
  • They may appear inside a branch or nested block that is itself inside a loop.
  • They target the nearest enclosing loop.
  • Moth does not have labelled loop control.
  • Neither statement produces a value.
  • return and return! leave the function rather than only leaving the loop.

Template loops use structural template forms for loop control. Those are specified by the Templates documentation rather than this statement-loop reference.