This page covers the building blocks that shape how your program makes decisions and repeats work: conditions, loops, breaks, and a few loop helpers that make common patterns shorter. Every example is verified: most with an assert naming the exact expected result, and the first with a panic that would fire if the rule it shows did not hold.
fn main() {
Conditionals
if runs a block only when its condition is true. If the condition is false the block is skipped entirely — nothing else happens. panic stops the program immediately with a message. During development it is a clear way to mark situations that should never occur.
if 2 > 5 {
panic("Incorrect test");
}
Combine conditions with and / or (or their symbol equivalents && / ||). Note that & is the bitwise AND operator — it works on the individual 1s and 0s inside a number, which is different from the logical and keyword that works on true/false values. An if/else chain picks exactly one branch to execute.
a = 12;
if a > 10 and a & 7 == 4 {
a += 1;
} else {
a -= 1;
}
Text enclosed in double quotes can contain expressions inside curly braces — the expression is evaluated and its result is inserted into the text. assert checks that its first argument is true; if it is false the second argument is printed as an error message.
assert(a == 13, "Incorrect value {a} != 13");
if as an expression
An if, a match, and a bare { } block all produce a value — the last expression inside them. That means you can use if/else on the right-hand side of an assignment, picking between two values based on a condition. No ternary operator needed. Loops are the exception: for and while are statements, so x = for i in 1..3 { i } does not compile.
b = if a == 13 {
"Correct"
} else {
"Wrong"
};
assert(b == "Correct", "Logic expression");
Iteration
for x in a..b loops over the integers starting at a and stopping before b (exclusive upper bound). Use ..= to include the upper bound. Here we sum 1 + 2 + 3 + 4 + 5 = 15.
t = 0;
for i in 1..6 {
t += i;
}
assert(t == 15, "Total was {t} instead of 15");
rev(range) reverses the direction of any range. 1..=5 is inclusive, so it visits 5, 4, 3, 2, 1 in that order. Multiplying each digit into a running total builds the number 54321.
t = 0;
for i in rev(1..=5) {
t = t * 10 + i;
}
assert(t == 54321, "Result was {t} instead of 54321");
A range with no upper bound, a.., counts up until the body leaves it with break or return — the first square above 50 is 64, found at i = 8.
first = 0;
for i in 1.. {
if i * i > 50 {
first = i;
break;
}
}
assert(first == 8, "First was {first} instead of 8");
The other loop: while
Loft has two loop statements: for, which walks a range or a collection, and while, which repeats as long as its condition holds. A for over a BOUNDED range carries its own end, so it is the one to reach for; a while does not, and while true { } runs until something stops it. Because nothing bounds a while, its body must move the condition towards false — here n += 1 is what ends the loop. This is the same sum as the for above, written the other way.
n = 1;
t = 0;
while n <= 5 {
t += n;
n += 1;
}
assert(t == 15, "while summed to {t} instead of 15");
Nested loops and break
break exits the nearest enclosing loop immediately. To exit an outer loop from inside an inner one, write outerVar#break. Here x#break leaves both loops when the product x*y reaches 16.
b = "";
for x in 1..5 {
for y in 1..5 {
if y > x {
break;
this breaks the inner y loop
}
if x * y >= 16 {
x#break;
}
if len(b) > 0 {
b += "; ";
}
b += "{x}:{y}";
}
}
assert(b == "1:1; 2:1; 2:2; 3:1; 3:2; 3:3; 4:1; 4:2; 4:3", "Incorrect sequence '{b}'");
Skipping an iteration: continue
continue is the sibling of break: instead of leaving the loop it skips the rest of the current iteration and moves on to the next value. Here we sum 1..=5 but skip 3, so the total is 1 + 2 + 4 + 5 = 12.
t = 0;
for i in 1..=5 {
if i == 3 {
continue;
}
t += i;
}
assert(t == 12, "continue skipped 3: total was {t} instead of 12");
Loop helpers: #first and #count
Inside a loop body you have access to some useful metadata about the current iteration. x#first is true only for the very first element that passes the filter. x#count is a zero-based index counting only the elements that were not filtered out. An if clause directly after in range filters which values enter the loop — here we skip every value where x % 3 == 1, keeping 2, 3, 5, 6, 8, 9.
b = "";
for x in 1..=9 if x % 3 != 1 {
if !x#first {
b += ", ";
}
b += "{x#count}:{x}";
}
assert(b == "0:2, 1:3, 2:5, 3:6, 4:8, 5:9", "Sequence '{b}'");
Formatting a loop inline
A for loop can appear inside a format string. The results are collected into a bracketed, comma-separated list. A format specifier after the closing brace is applied to every element — :02 means at least 2 digits, zero-padded. This is handy for building compact representations on the fly.
assert("a{for x in 1..7 {x*2}:02}b" == "a[02,04,06,08,10,12]b", "Format range");
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2024-2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Keywords
// @TITLE: Keywords and control flow
// This page covers the building blocks that shape how your program makes
// decisions and repeats work: conditions, loops, breaks, and a few loop
// helpers that make common patterns shorter. Every example is verified: most
// with an assert naming the exact expected result, and the first with a panic
// that would fire if the rule it shows did not hold.
fn main() {
// ## Conditionals
// 'if' runs a block only when its condition is true. If the condition is
// false the block is skipped entirely — nothing else happens.
// 'panic' stops the program immediately with a message. During development
// it is a clear way to mark situations that should never occur.
if 2 > 5 {
panic("Incorrect test");
}
// Combine conditions with 'and' / 'or' (or their symbol equivalents '&&' / '||').
// Note that '&' is the bitwise AND operator — it works on the individual
// 1s and 0s inside a number, which is different from the logical 'and' keyword
// that works on true/false values.
// An 'if/else' chain picks exactly one branch to execute.
a = 12;
if a > 10 and a & 7 == 4 {
a += 1;
} else {
a -= 1;
}
// Text enclosed in double quotes can contain expressions inside curly braces —
// the expression is evaluated and its result is inserted into the text.
// 'assert' checks that its first argument is true; if it is false the
// second argument is printed as an error message.
assert(a == 13, "Incorrect value {a} != 13");
// ## if as an expression
// An 'if', a 'match', and a bare '{ }' block all produce a value — the last
// expression inside them. That means you can use 'if/else' on the right-hand
// side of an assignment, picking between two values based on a condition. No
// ternary operator needed. Loops are the exception: 'for' and 'while' are
// statements, so 'x = for i in 1..3 { i }' does not compile.
b = if a == 13 {
"Correct"
} else {
"Wrong"
};
assert(b == "Correct", "Logic expression");
// ## Iteration
// 'for x in a..b' loops over the integers starting at a and stopping
// before b (exclusive upper bound). Use '..=' to include the upper bound.
// Here we sum 1 + 2 + 3 + 4 + 5 = 15.
t = 0;
for i in 1..6 {
t += i;
}
assert(t == 15, "Total was {t} instead of 15");
// 'rev(range)' reverses the direction of any range.
// '1..=5' is inclusive, so it visits 5, 4, 3, 2, 1 in that order.
// Multiplying each digit into a running total builds the number 54321.
t = 0;
for i in rev(1..=5) {
t = t * 10 + i;
}
assert(t == 54321, "Result was {t} instead of 54321");
// A range with no upper bound, 'a..', counts up until the body leaves it with
// 'break' or 'return' — the first square above 50 is 64, found at i = 8.
first = 0;
for i in 1.. {
if i * i > 50 {
first = i;
break;
}
}
assert(first == 8, "First was {first} instead of 8");
// ## The other loop: while
// Loft has two loop statements: 'for', which walks a range or a collection, and
// 'while', which repeats as long as its condition holds. A 'for' over a BOUNDED range
// carries its own end, so it is the one to reach for; a 'while' does not,
// and 'while true { }' runs until something stops it. Because nothing bounds a
// 'while', its body must move the condition towards false — here 'n += 1' is what
// ends the loop. This is the same sum as the 'for' above, written the other way.
n = 1;
t = 0;
while n <= 5 {
t += n;
n += 1;
}
assert(t == 15, "while summed to {t} instead of 15");
// ## Nested loops and break
// 'break' exits the nearest enclosing loop immediately.
// To exit an outer loop from inside an inner one, write 'outerVar#break'.
// Here x#break leaves both loops when the product x*y reaches 16.
b = "";
for x in 1..5 {
for y in 1..5 {
if y > x {
break;
// this breaks the inner y loop
}
if x * y >= 16 {
x#break;
}
if len(b) > 0 {
b += "; ";
}
b += "{x}:{y}";
}
}
assert(b == "1:1; 2:1; 2:2; 3:1; 3:2; 3:3; 4:1; 4:2; 4:3", "Incorrect sequence '{b}'");
// ## Skipping an iteration: continue
// 'continue' is the sibling of 'break': instead of leaving the loop it skips
// the rest of the current iteration and moves on to the next value. Here we
// sum 1..=5 but skip 3, so the total is 1 + 2 + 4 + 5 = 12.
t = 0;
for i in 1..=5 {
if i == 3 {
continue;
}
t += i;
}
assert(t == 12, "continue skipped 3: total was {t} instead of 12");
// ## Loop helpers: #first and #count
// Inside a loop body you have access to some useful metadata about the
// current iteration. 'x#first' is true only for the very first element
// that passes the filter. 'x#count' is a zero-based index counting only
// the elements that were not filtered out.
// An 'if' clause directly after 'in range' filters which values enter the loop —
// here we skip every value where x % 3 == 1, keeping 2, 3, 5, 6, 8, 9.
b = "";
for x in 1..=9 if x % 3 != 1 {
if !x#first {
b += ", ";
}
b += "{x#count}:{x}";
}
assert(b == "0:2, 1:3, 2:5, 3:6, 4:8, 5:9", "Sequence '{b}'");
// ## Formatting a loop inline
// A for loop can appear inside a format string. The results are collected
// into a bracketed, comma-separated list. A format specifier after the
// closing brace is applied to every element — ':02' means at least 2 digits,
// zero-padded. This is handy for building compact representations on the fly.
assert("a{for x in 1..7 {x*2}:02}b" == "a[02,04,06,08,10,12]b", "Format range");
}