Skip to main content

Control Flow

If / elif / else

if (x > 0) {
print("positive")
} elif (x < 0) {
print("negative")
} else {
print("zero")
}

While

var i = 0
while (i < 10) {
print(i)
i++
}

For (C-style)

for (var i = 0; i < 5; i++) {
print(i)
}

The initializer, condition, and increment execute in the enclosing scope.

Step through an array by index with len() bound as the stop condition — the loop condition re-evaluates every iteration, so it tracks the array length automatically:

var items = ["a", "b", "c"]
for (var i = 0; i < len(items); i++) {
print(i, "-", items[i])
}
// 0 - a
// 1 - b
// 2 - c

Foreach (for with colon and foreach)

Destructured form (colon-only — the comma form is not supported):

var arr = [10, 20, 30]
for (val : arr) {
print(val)
}

var obj = {a: 1, b: 2}
for (pair : obj) {
print(pair.key, pair.value) // single variable: {key, value} pair
}

for ([k, v] : obj) {
print(k, v) // destructured: key string, value
}

for ([i, v] : arr) {
print(i, v) // arrays: index, element
}

The loop variable(s) are available only inside the body. If the header starts with [, it must be exactly [key, value]; a bracketed header without a colon reports a clear error (Expected : after [key, value] in foreach).

Foreach keyword

foreach iterates with the implicit element bound to each:

foreach(arr) {
print(each)
}
foreach(obj) {
print(each.key, each.value)
}

each is reserved — it can be read but never assigned, shadowed, or used as a parameter name. for (each : arr) is rejected.

Switch

There is no implicit fall-through between separate cases — the switch exits after the matching case's body runs, so break is conventional but optional:

var x = 2
switch (x) {
1: print("one")
2: print("two")
default: print("other")
}

Case values are expressions: numbers, strings, chars, true/false/nil, negated values, and parenthesized expressions like (a + b). String literals (' "str" '), chars, and multi-token expressions such as (a + b): are all valid.

Fall-through cases

A case list separated by colons — 1:2:3: — is a set of fall-through cases: execution falls through the listed case labels into a single shared body. It is a shorthand for giving several values the same handling:

var x = 2
switch (x) {
1:2:3: print("low") break
4:5:6: print("high") break
default: print("other") break
}

Break and continue

continue and break jump control within a loop. The difference is how far the jump is and what it resumes:

  • continueleave the current iteration and start the next one. The rest of the loop body is skipped, but the loop itself keeps running.
  • breakleave the whole loop (the enclosing block) entirely and resume execution right after it.
var i = 0
while (true) {
i++
if (i < 5) { continue } // skip the rest of this iteration, loop again
if (i >= 7) { break } // exit the loop completely
print(i)
}
// prints 5, 6 (i==1..4 hit continue first; i==7 breaks before printing)

This works identically in for, foreach, and for ... : loops — they all behave the same way inside their body. Another way to think about it: continue skips to the loop's next check, while break steps right out of the loop block and lands on whatever comes after it.

While semantics and cooperative yielding

The condition is evaluated before each iteration (a while body may run zero times). Use break to leave early and continue to skip one iteration.

Busy loops don't lock the engine

Scrii execution is cooperative: each fiber voluntarily yields to the scheduler at regular points — roughly every 64 statements, at std.async.sleep, and at std.async.await. So even a loop whose body never calls any async function, like

var n = 0
while (true) { n++ }

does not permanently lock the engine. Every 64 statements it hands control back to the scheduler, so concurrently spawned tasks (std.async fibers) and other workers keep making progress. The loop simply runs as soon as it gets a timeslice again.

There is a cost, though: a CPU-bound while (true) spins at full speed, so it's almost never the right tool. Prefer breaking out of a counted loop, or waiting explicitly with std.async.sleep(ms) when you want to pause without burning the CPU. A bare infinite loop is mainly useful as a background worker that does a small amount of work per iteration and then yields — for example a poll loop that sleeps between checks instead of spinning.