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 —
a var declared in the initializer stays visible (with its final value)
after the loop ends.

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 arr = ["a", "b", "c"]
for (var i = 0; i < len(arr); i++) {
  print(i, "-", arr[i])
}
# 0 - a
# 1 - b
# 2 - c

Foreach: for with colon, and the foreach keyword

Two spellings for the same loop. Use for (name : iterable) when you want
to name the loop variable; use foreach (iterable) as shorthand when the
implicit each variable is enough:

var arr = [10, 20, 30]
for (val : arr) {
  print(val)          # 10, 20, 30
}
foreach (arr) {
  print(each)         # same thing: 10, 20, 30
}

What the loop variable holds

A single variable holds the element for arrays, but a
{key, value} pair object for objects:

var obj = {a: 1, b: 2}
for (pair : obj) {
  print(pair.key, pair.value)   # a 1, then b 2 (any order)
}

To bind key and value separately, destructure with a comma pair —
k, v, exactly two identifiers:

for (k, v : obj) {
  print(k, v)                   # objects: key string, value
}

for (i, v : arr) {
  print(i, v)                   # arrays: 0 10, 1 20, 2 30
}

Only the name and name, name headers are valid — anything else before
the colon is rejected at parse time.

The foreach keyword and each

foreach iterates with the element (or pair) bound to the implicit variable
each:

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

each is reserved — it can be read but never assigned, shadowed, declared,
or used as a parameter name, and for (each : arr) is rejected. The loop
variable(s), named or implicit, exist only inside the loop body.

Iterating anything that is not an array or object is an error
(INVALID_ITERABLE: foreach requires an array or object).

Switch

Scrii has no enumerators, so switch is built to dispatch on plain data
instead: any value — a command string, a key char, a status number, nil —
can be a case. Matching intent reads directly off the values, which keeps
dispatch tables readable without a separate enum declaration to maintain.

Cases fall through C-style: after a matching case runs, execution continues
into the next case body unless the body ends with break — so break is
needed to stop at one case:

var x = 1
switch (x) {
  1: print("one")
  2: print("two") break
  default: print("other") break
}
# prints "one", then "two"

Case values

A case matches when its value equals the switch subject (==). Almost
anything that evaluates to a value can be a case — literals of any type,
variables, and expressions:

var cmd = "quit"
switch (cmd) {
  "help": print("commands: help, quit") break
  "quit": print("bye") break
  default: print("unknown command") break
}

var key = 'q'
switch (key) {
  'q': print("quit key") break
  default: print("other key") break
}

var ready = true
switch (ready) {
  true: print("go") break
  false: print("wait") break
}

var missing = nil
switch (missing) {
  nil: print("nothing there") break
  default: print("something there") break
}

Negated values and parenthesized expressions work too, and are evaluated when
the switch runs:

var v = 5
switch (v) {
  -1: print("minus one") break
  (2 + 3): print("five") break
  default: print("something else") break
}
# prints "five"

var target = 42
switch (target) {
  target: print("self match") break
  default: print("no match") break
}
# prints "self match"

default catches everything that matched nothing else. It may appear
anywhere in the switch body — first, middle, or last — and still only runs on
no match:

switch (99) {
  1: print("one") break
  default: print("fallback") break
  2: print("two") break
}
# prints "fallback"

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
:

  • continue — leave the current iteration and start the next one. The
    rest of the loop body is skipped, but the loop itself keeps running.
  • break — leave 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.