For loops

for loops are the most powerful control flow mechanism in Onyx. They enable:

  • Iteration shorthand
  • Custom iterators
  • Removing elements
  • Scoped resources

Range-Based Loop

A basic for loop in Onyx. This will iterate from 1 to 9, as the upper bound is not included.

for i in 1 .. 10 {
    println(i);
}

This for loop is iterating over a range. Ranges represent half-open sets, so the lower bound is included, but the upper bound is not.

Array-Based Loop

for loops can also iterate over array-like types: [N] T, [] T, [..] T. Use & after for to iterate over the array by pointer.

primes: [5] i32 = .[ 2, 3, 5, 7, 11 ];
for value in primes {
    println(value);
}

// This modifies the array so each element
// is double what it was.
for &value in primes {
    // value is a &i32.
    *value *= 2;
}

it

Naming the iteration value is optional. If left out, the iteration value will be called it.

for i32.[2, 3, 5, 7, 11] {
    println(it);
}

Indexed-loops

for loops can optionally have a second iteration value called the index. This index starts at 0, and increments by 1 every iteration. Its default type is i32, but this can be changed.

// Use i32 as type of index
for value, index in i32.[2, 3, 5, 7, 11] {
    printf("{}: {}", index, value);
}

// Explictly change the type to i64
for value, index: i64 in i32.[2, 3, 5, 7, 11] {
    printf("{}: {}", index, value);
}

Custom Iterators Loops

The final type that for loops can iterate over is Iterator(T). Iterator is a built-in type that represents a generic iterator. An Iterator has 4-elements:

  • data - a pointer to the context for the iterator.
  • next - a function to retrieve the next value out of the iterator.
  • remove - an optional function to remove the current element.
  • close - an optional function to cleanup the iterator's context. The core.iter package provides many utilities for working with iterators.

Here is a basic example of creating an iterator from a range, then using iter.map to double the values. Iterators are lazily evaluated, so none of the actual doubling happens until values are pulled out of the iterator by the for loop.

doubled_iterator := iter.as_iter(1 .. 5)
                 |> iter.map(x => x * 2);

for doubled_iterator {
    println(it);
}

The above for loop loosely translates to the following code.

doubled_iterator := iter.as_iter(1 .. 5)
                 |> iter.map(x => x * 2);

{
    defer doubled_iterator.close(doubled_iterator.data);

    while true {
        it, cont := doubled_iterator.next(doubled_iterator.data);
        if !cont do break;    

        println(it);
    }
}

#no_close

The close function of an Iterator is always called after the loop exits. If this is not the desired behavior, you can add #no_close after for to forego inserting the close call.

doubled_iterator := iter.as_iter(1 .. 5)
                 |> iter.map(x => x * 2);
for #no_close doubled_iterator {
    println(it);
}

// Later:
iter.close(doubled_iterator);

#remove

The final feature of Iterator-based for loops is the #remove directive. If the current Iterator supports it, you can write #remove to remove the current element from the iterator.

// Make a dynamic array from a fixed-size array.
arr := array.make(u32.[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

// as_iter with a pointer to a dynamic array 
// supports #remove.
for iter.as_iter(&arr) {
    if it % 2 == 0 {
        // Remove all even numbers
        #remove;
    }
}

// Will only print odd numbers
for arr do println(it);

#first

Many times while writing for loops, it is nice to know if this iteration is one of two special values: the first and last iteration. As a convenience, Onyx provides the #first directive in all of its for loops. It is a bool value that is true during the first iteration, and then false afterwards. Note, Onyx does not provide an equivalent #last directive, because in Iterator-based loops, it is impossible to know when the last iteration will happen.

One example of where this is useful is in a formatted printer. Consider this code that prints the elements of an array.

arr := i32.[ 2, 3, 5, 7, 11 ];

for arr {
    if !#first do print(", ");
    print(it);
}

This example will print:

2, 3, 5, 7, 11

Explicitly-typed loop variable

You can optionally provide an explicit type for the loop variable, if you feel it improves code readability. It does not carry any extra semantic meaning, but solely exists for the next reader of the code. If you provide the incorrect type, it is a compile error.

strings := str.["A", "list", "of", "strings"];

// Explcitly say that value is a str
for value: str in strings {
    println(value)
}