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.
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")damaged
alive
condition: yellowfunction 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))describeAmmo(12) = 12 rounds left
describeAmmo(0) = empty
describeAmmo(-1) = invalid beltwhile 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.
// 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)rounds left: 0
reload attempt 1
reload attempt 2
reload attempt 3for
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.
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}")total damage: 60
swap 0 <-> 4
swap 1 <-> 3foreach
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:
| Container | one variable | two variables |
|---|---|---|
| array | element | position (from 0), element |
| table, class, instance | value | key, value |
| string | character code (an integer, not a one-char string) | position (from 0), character code |
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}")squadMembers roles:
driver
gunner
loader
seat 0: driver
seat 1: gunner
seat 2: loaderlet 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]}")machineGun: 250
mainGun: 40let 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}")callsign char codes:
65
86
56
0: 65
1: 86
2: 56A 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.
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}")first armed contact: tank
scanned before first threat: 2Loop 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.
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("")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.
#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"))describeHitZone("head") = critical
describeHitZone("chest") = major
describeHitZone("leg") = minorNotes
- A
local/letdeclared in anifcondition, aforinit, or bound byforeachdoes not leak past the statement's own body. whiledoes not accept a declaration in its condition; onlyifdoes.