Quirrel4.41.0

Control flow

Quirrel has the usual C-family statements: if/else, while, do ... while, a C style for, foreach, and a switch that is off by default.

if / else

A condition may be a plain expression, or a local/let declaration checked for truth (null, integer 0 and float 0.0 are false; everything else is true). The declaration may be followed by ; and a separate condition. A binding declared this way is visible in the whole chain: every else if and the final else can see it.

examples/pages/language/control-flow-if-basic.nut
let hitPoints = 40

// if on its own
if (hitPoints < 100)
  println("damaged")

// if with an else
if (hitPoints > 0)
  println("alive")
else
  println("destroyed")

// a chain: the first matching branch wins, the rest are skipped
if (hitPoints > 75)
  println("condition: green")
else if (hitPoints > 25)
  println("condition: yellow")
else
  println("condition: red")
Output:
damaged
alive
condition: yellow
examples/pages/language/control-flow-if.nut
function describeAmmo(beltCount) {
  // the declaration before ';' is scoped to the whole if/else chain
  if (let rounds = beltCount; rounds > 0)
    return $"{rounds} rounds left"
  else if (rounds == 0)
    return "empty"
  else
    return "invalid belt"
}

println("describeAmmo(12) =", describeAmmo(12))
println("describeAmmo(0) =", describeAmmo(0))
println("describeAmmo(-1) =", describeAmmo(-1))
Output:
describeAmmo(12) = 12 rounds left
describeAmmo(0) = empty
describeAmmo(-1) = invalid belt

while and do ... while

while checks the condition before each pass, so the body may run zero times. do ... while checks it after, so the body always runs at least once.

examples/pages/language/control-flow-while.nut
// while checks before each pass: zero rounds skips the loop entirely
local rounds = 0
while (rounds > 0) {
  println("firing")
  rounds -= 1
}
println($"rounds left: {rounds}")

// do/while checks after: the body always runs at least once
local reloadAttempt = 0
do {
  reloadAttempt += 1
  println($"reload attempt {reloadAttempt}")
} while (reloadAttempt < 3)
Output:
rounds left: 0
reload attempt 1
reload attempt 2
reload attempt 3

for

stat := 'for' '(' [init] ';' [cond] ';' [step] ')' stat. Any part may be empty; for (;;) { ... } is an infinite loop. init and step accept several comma-separated declarations or expressions.

examples/pages/language/control-flow-for.nut
local totalDamage = 0
for (local shot = 0; shot < 5; shot += 1)
  totalDamage += 10 + shot
println($"total damage: {totalDamage}")

// several variables and several increments, comma-separated
for (local front = 0, back = 4; front < back; front += 1, back -= 1)
  println($"swap {front} <-> {back}")
Output:
total damage: 60
swap 0 <-> 4
swap 1 <-> 3

foreach

foreach iterates over an array, a table, a class, a string, or a generator. The one-variable form binds the value. The two-variable form binds an index or key first, then the value. The meaning of the first binding depends on the container:

Containerone variabletwo variables
arrayelementposition (from 0), element
table, class, instancevaluekey, value
stringcharacter code (an integer, not a one-char string)position (from 0), character code
examples/pages/language/control-flow-foreach-array.nut
let squadMembers = ["driver", "gunner", "loader"]

// one variable: the value
println("squadMembers roles:")
foreach (role in squadMembers)
  println(role)

// two variables: the position, then the value
foreach (seat, role in squadMembers)
  println($"seat {seat}: {role}")
Output:
squadMembers roles:
driver
gunner
loader
seat 0: driver
seat 1: gunner
seat 2: loader
examples/pages/language/control-flow-foreach-table.nut
let ammoCounts = { mainGun = 40, machineGun = 250 }

// two variables over a table: the key, then the value (not a sequence number)
// table order is not guaranteed, so collect the keys and sort before printing
let weaponNames = []
foreach (weaponName, count in ammoCounts)
  weaponNames.append(weaponName)
weaponNames.sort()

foreach (weaponName in weaponNames)
  println($"{weaponName}: {ammoCounts[weaponName]}")
Output:
machineGun: 250
mainGun: 40
examples/pages/language/control-flow-foreach-string.nut
let callsign = "AV8"

// one variable: each character's integer code, not a one-char string
println("callsign char codes:")
foreach (code in callsign)
  println(code)

// two variables: the position, then the same integer code
foreach (pos, code in callsign)
  println($"{pos}: {code}")
Output:
callsign char codes:
65
86
56
0: 65
1: 86
2: 56

A table's iteration order is not guaranteed, so a program that must be deterministic sorts the keys itself, as the table sample above does.

A bound variable may also be a destructuring pattern instead of a plain name. See Destructuring for the pattern syntax and defaults.

break, continue, return

break leaves the innermost for, foreach, while or do ... while (or a switch, see below). continue skips to the next pass of the innermost loop. return leaves the function, not only the loop.

examples/pages/language/control-flow-loop-control.nut
let contacts = [
  { name = "wreck", hp = 0, hostile = false },
  { name = "scout", hp = 40, hostile = false },
  { name = "tank", hp = 100, hostile = true },
]

function firstArmedContact(list) {
  foreach (contact in list) {
    if (contact.hp <= 0)
      continue            // skip wrecks, keep scanning
    if (contact.hostile)
      return contact.name // leaves the function, not just the loop
  }
  return null
}
println("first armed contact:", firstArmedContact(contacts))

local scanned = 0
foreach (contact in contacts) {
  if (contact.hostile)
    break                  // leaves only the loop
  scanned += 1
}
println($"scanned before first threat: {scanned}")
Output:
first armed contact: tank
scanned before first threat: 2

Loop bodies and closures

Each pass of a loop body creates new local/let bindings. A closure created inside the body and kept after the loop sees the value that the binding held on the pass that created it. The bound variables of foreach follow the same rule, because each pass gives them a new value (or key/value pair).

for is the exception. Its control variable lives in one slot for the whole loop, and every pass changes that slot. A closure that captures the control variable directly reads that same slot. After the loop ends, every such closure sees the final value, not the value from its own pass. To capture the per-pass value, copy it to a new let inside the body.

examples/pages/language/control-flow-closures.nut
let turretAngles = [15, 45, 75]

// for's control variable is a single slot; every closure sees where it
// ended up, not what it held on the pass that created the closure
let staleQueue = []
for (local slot = 0; slot < turretAngles.len(); slot += 1)
  staleQueue.append(@() slot)

// a local declared inside the body is fresh each pass, so it is safe to capture
let freshQueue = []
for (local slot = 0; slot < turretAngles.len(); slot += 1) {
  let ownSlot = slot
  freshQueue.append(@() ownSlot)
}

// foreach's own loop variable is already fresh each pass, no workaround needed
let angleQueue = []
foreach (angle in turretAngles)
  angleQueue.append(@() angle)

println("staleQueue (captured for's shared slot):")
foreach (getSlot in staleQueue) print($"{getSlot()} ")
println("")
println("freshQueue (captured a fresh local per pass):")
foreach (getSlot in freshQueue) print($"{getSlot()} ")
println("")
println("angleQueue (captured foreach's own fresh variable):")
foreach (getAngle in angleQueue) print($"{getAngle()} ")
println("")
Output:
staleQueue (captured for's shared slot):
3 3 3 
freshQueue (captured a fresh local per pass):
0 1 2 
angleQueue (captured foreach's own fresh variable):
15 45 75 

switch

switch is disabled by default. With no directive, switch, case and default are ordinary identifiers, and switch (x) { ... } fails to compile. The #allow-switch-statement compiler directive turns the keywords on for the enclosing block; the setting reverts at the closing brace. A leading #default:allow-switch-statement sets it for the whole compilation unit. See Compiler directives.

switch (x) {
  case 1:
    println("one")
}

A case without a break falls through into the next case. Several labels can share one body when they are written together with no code between them. default, if present, must be the last clause; a case after it fails to compile.

examples/pages/language/control-flow-switch.nut
#allow-switch-statement

function describeHitZone(zone) {
  switch (zone) {
    case "head":
    case "neck":
      return "critical"
    case "chest":
      return "major"
    default:
      return "minor"
  }
}

println("describeHitZone(\"head\") =", describeHitZone("head"))
println("describeHitZone(\"chest\") =", describeHitZone("chest"))
println("describeHitZone(\"leg\") =", describeHitZone("leg"))
Output:
describeHitZone("head") = critical
describeHitZone("chest") = major
describeHitZone("leg") = minor

Notes