Operators and expressions
Most operators do what the same symbol does in C. The differences are which values count as false, what ?? and ?. check, and some precedence traps.
All operators
Every operator the compiler accepts, with the metamethod that overloads it where there is one. Each links to the section that explains it.
| operator | metamethod | |
|---|---|---|
| Arithmetic | ||
+ | add, or concatenate when either side is a string | _add |
- | subtract | _sub |
* | multiply | _mul |
/ | divide; integer / integer truncates | _div |
% | remainder | _modulo |
-x | negate | _unm |
++ | increment, as a prefix or a suffix | |
-- | decrement, as a prefix or a suffix | |
| Comparison | ||
== | equal | _cmp |
!= | not equal | _cmp |
< | less than | _cmp |
<= | less than or equal | _cmp |
> | greater than | _cmp |
>= | greater than or equal | _cmp |
<=> | three-way compare, giving a negative number, zero or a positive one | _cmp |
| Logical | ||
&& | and; yields the operand that decided the result, not a bool | |
|| | or; yields the operand that decided the result, not a bool | |
! | not; this one does yield a bool | |
| Bitwise | ||
& | and | |
| | or | |
^ | exclusive or | |
~ | complement | |
<< | shift left | |
>> | shift right, keeping the sign | |
>>> | shift right, filling with zeroes | |
| Assignment | ||
= | assign to something that already exists | |
<- | create a slot, then assign | _newslot |
+= | add and assign | |
-= | subtract and assign | |
*= | multiply and assign | |
/= | divide and assign | |
%= | take the remainder and assign | |
| Access | ||
. | member of a table, class or instance | _get |
[] | index a table, array or string | _get |
?. | member, giving null when the left side is null | |
?[] | index, giving null when the left side is null | |
?() | call, giving null when the callee is null | |
?? | the right side when the left one is null; only null, not any false value | |
.$ | reach the built-in type method, ignoring a slot of the same name that would shadow it, as in o.$rawdelete(key) | |
:: | read a name from the root table | |
| Other | ||
?: | conditional; picks one of two expressions | |
in | has this key; on an array it tests the INDEX, not the value | |
not in | the negation of in | |
instanceof | is this instance of that class | |
typeof | the type name, through _typeof when there is one | _typeof |
clone | shallow copy; a keyword, so x.clone() does not parse | _cloned |
delete | remove a slot; forbidden unless a directive allows it | _delslot |
... | spread a container into a table or array literal; nowhere else, and in a parameter list it marks a vararg instead | |
There is no bitwise compound assignment: &=, |=, ^=, <<= and >>= are all compile errors. Write flags = flags & mask instead.
Arithmetic
+ - * / and % work on int and float. Division of two int truncates toward zero. An int mixed with a float gives a float. % keeps the sign of the left operand, like C; it is not the mathematical modulo.
+ also concatenates: if either side is a string, it converts the other side to a string, so 1 + "2" is "12" and not an error. Prefer $"...{...}" interpolation to build strings.
let magazineSize = 30
let roundsFired = 7
// integer / integer truncates toward zero, it does not round
let fullReloads = magazineSize / roundsFired
println("fullReloads =", fullReloads)
// mix in a float and the result promotes to float
let secondsPerRound = 1.5
println("roundsFired * secondsPerRound =", roundsFired * secondsPerRound)
// % keeps the sign of the left operand, like C
println("-roundsFired % 3 =", -roundsFired % 3)
// DEPRECATED:
// + is the one arithmetic operator that also does string concat:
// a string operand makes + stringify and join instead of adding
println("rounds left: " + (magazineSize - roundsFired))fullReloads = 4
roundsFired * secondsPerRound = 10.5
-roundsFired % 3 = -1
rounds left: 23Comparison and three-way compare
== != < <= > >= compare two values and return a bool, not 0/1. <=> is the three-way compare. It returns an int that is negative, zero, or positive when the left side is less than, equal to, or greater than the right. This is the result sort expects from a comparator.
let squads = [
{ name = "bravo", strength = 9 },
{ name = "alpha", strength = 4 },
{ name = "charlie", strength = 6 },
]
// == != < <= > >= all return a real bool
println("squads[0].strength > squads[1].strength =", squads[0].strength > squads[1].strength)
// <=> returns an int: negative, zero or positive, not just a bool,
// which is exactly what sort() wants from its comparator
squads.sort(@(a, b) a.strength <=> b.strength)
foreach (squad in squads)
println($"{squad.name}: {squad.strength}")
// the ternary picks one of two expressions, not two statements
let weakest = squads[0]
println("weakest.strength > 5 =", weakest.strength > 5 ? "combat ready" : "needs reinforcement")squads[0].strength > squads[1].strength = true
alpha: 4
charlie: 6
bravo: 9
weakest.strength > 5 = needs reinforcementLogical operators
&&, || and ! treat null, false, the integer 0 and the float 0.0 as false. Everything else is true, including "", [] and {}. Quirrel does not treat an empty string, array or table as false.
&& and || short-circuit and evaluate to one of their operands, not always a bool. && evaluates its left side; if that is false, it stops and yields it; otherwise it evaluates and yields the right side. || is the mirror image. Only ! always produces a bool.
// only null, false, integer 0 and float 0.0 are falsy;
// "" and [] and {} are truthy, unlike some other scripting languages
println("[] =", [] ? "truthy" : "falsy")
println("0 =", 0 ? "truthy" : "falsy")
// && and || return one of their OPERANDS, not necessarily a bool,
// and short-circuit: the second side is not even evaluated
function ammoBelt() {
println("ammoBelt() evaluated")
return "belt_762"
}
let preferredAmmo = null
println("preferredAmmo && ammoBelt() =", preferredAmmo && ammoBelt())
println("preferredAmmo || ammoBelt() =", preferredAmmo || ammoBelt())
// ! always returns a real bool
let hitPoints = 0
println("!hitPoints =", !hitPoints)
println("typeof !hitPoints =", typeof !hitPoints)[] = truthy
0 = falsy
preferredAmmo && ammoBelt() = null
ammoBelt() evaluated
preferredAmmo || ammoBelt() = belt_762
!hitPoints = true
typeof !hitPoints = boolNull-coalescing and null-safe access
?? looks like || but tests strictly for null. The false-but-valid values above (0, 0.0, false) pass through it. A plain || used as a default replaces a real 0 or false with the fallback; ?? does not.
?. and ?[ are the null-safe forms of . and [. If the value on the left is null, the whole expression is null and nothing throws. Once one of them fires on a null, the compiler treats the rest of the chain (further ., [, or a call) as null-safe too. a?.b.c[0]() needs only one ?., not one at every step.
let vehicle = { turretAngle = 0.0, gunner = null }
// ?? only tests for null, unlike a truthy check: a real 0.0 survives it
println("vehicle.turretAngle ?? 45.0 =", vehicle.turretAngle ?? 45.0)
// a plain || default would get this wrong: 0.0 is falsy, so it falls
// through to 45.0 even though 0.0 is a legitimate angle
println("vehicle.turretAngle || 45.0 =", vehicle.turretAngle || 45.0)
// ?. stops a missing/null step from throwing, and once it fires the
// rest of the chain (., [], ()) is null-safe too, without repeating ?.
println("vehicle.gunner?.rank.tostring() =", vehicle.gunner?.rank.tostring())
// ?[ ] is the null-safe form of the index operator
let squad = null
println("squad?[\"leader\"] =", squad?["leader"])vehicle.turretAngle ?? 45.0 = 0
vehicle.turretAngle || 45.0 = 45
vehicle.gunner?.rank.tostring() = null
squad?["leader"] = nullin, instanceof, typeof
key in container and key not in container test whether a slot exists. On a table this means the key is present. On an array it means the key is a valid index. This is a common trap: 20 in [10, 20, 30] is false because 20 is not a valid index, even though it is one of the values. Use contains to search by value.
instanceof tests whether an instance was made from a class or one of its subclasses. typeof returns the type name as a string ("table", "instance", "array", ...), through the _typeof metamethod when the class defines one. type answers the same question as a function. It can be passed as a callback, and it reports the plain type regardless of _typeof.
let loadout = { rifle = "ak74", helmet = "ssh68" }
println("\"rifle\" in loadout =", "rifle" in loadout)
println("\"boots\" not in loadout =", "boots" not in loadout)
// on an array 'in' tests the INDEX, not the value: it asks
// "is there a slot at this position", not "does this value occur"
let ammoBelt = [30, 30, 20]
println(1 in ammoBelt) // index 1 exists
println(20 in ammoBelt) // 20 is a value in the array, but not a valid index
class Vehicle {}
class Tank(Vehicle) {}
let myTank = Tank()
println("myTank instanceof Vehicle =", myTank instanceof Vehicle)
println("myTank instanceof Tank =", myTank instanceof Tank)
println("typeof myTank =", typeof myTank)
println("typeof ammoBelt =", typeof ammoBelt)"rifle" in loadout = true
"boots" not in loadout = true
true
false
myTank instanceof Vehicle = true
myTank instanceof Tank = true
typeof myTank = instance
typeof ammoBelt = arrayThe .$ type-method operator
A table's own slots and its built-in methods share one namespace, so a slot named len or rawdelete hides the method of that name. o.$name(...) calls the built-in type method directly and never sees the slot. The null-safe form, o?.$name(...), works the same way.
let squad = { name = "alpha" }
println("squad.$len() =", squad.$len()) // the built-in table method
// a slot can carry the same name as a method, and then it wins
let shadowed = { name = "bravo", len = "a field, not a method" }
println("shadowed.len =", shadowed.len)
println("shadowed.$len() =", shadowed.$len()) // $ never sees the slot
try { shadowed.len() } catch (e) { println("shadowed.len() throws:", e) }squad.$len() = 1
shadowed.len = a field, not a method
shadowed.$len() = 2
shadowed.len() throws: attempt to call 'string'Plain o.name(...) is safe on data whose keys you control. On data parsed from a file, received over a network, or passed in by a caller, use .$, because the data cannot break it. The compiler's message for the forbidden delete operator uses this form: Use 'o.$rawdelete("key")' instead. See types.Table.rawdelete.
clone
clone value makes a shallow copy of a table, array, or instance: the top-level slots are copied, but a container nested inside is shared with the original.
clone is a keyword, not an identifier, so there is no .clone() method by default:
let copy = original.clone() // parse error: expected 'IDENTIFIER'
The .$ form fails the same way, because the keyword is the problem, not the slot lookup. Write clone original instead, or original["clone"]().
With #forbid-clone-operator the operator is off and clone is an ordinary identifier, so original.clone() and original.$clone() compile and call the clone type method.
let loadoutTemplate = { weaponName = "ak74", ammoBelt = [30, 30] }
// clone is a shallow copy: top-level slots are copied, nested
// containers (like ammoBelt here) are shared with the original
let squadLoadout = clone loadoutTemplate
squadLoadout.weaponName = "svd"
squadLoadout.ammoBelt.append(20)
println("loadoutTemplate.weaponName =", loadoutTemplate.weaponName)
println("squadLoadout.weaponName =", squadLoadout.weaponName)
println("loadoutTemplate.ammoBelt.len() =", loadoutTemplate.ammoBelt.len())loadoutTemplate.weaponName = ak74
squadLoadout.weaponName = svd
loadoutTemplate.ammoBelt.len() = 3The newslot operator, <-
<- adds a new field to a table or instance. If the slot already exists, <- behaves like =. The reverse is not true: plain = never creates a slot. An assignment to a field that does not exist yet throws. <- is the only way to add a slot.
<- needs a table or instance slot on its left, not a bare variable. local a; a <- 1 is a compile error ("can't 'create' a local slot"), because a local binding is not a container that can hold new slots.
let mission = {}
// <- adds a field that did not exist before
mission.name <- "capture_the_flag"
mission["reward"] <- 500
// once the slot exists, <- behaves exactly like a plain assignment
mission.reward <- 750
println("mission.reward =", mission.reward)
// = never creates a slot: writing an unknown field throws
try {
mission.duration = 300
} catch (e) {
println("caught:", e)
}mission.reward = 750
caught: the index 'duration' (type='string') does not existCompound assignment and increment/decrement
+= -= *= /= %= read the current value, combine it, and write it back, so (like plain =) they need the slot to exist already. ++ and -- work as in C. As a statement the two forms do the same thing. As an expression the prefix form yields the value after the step and the postfix form yields the value before it.
let squad = { strength = 4 }
squad.strength += 3
squad.strength *= 2
println("squad.strength =", squad.strength)
local reloadSeconds = 3
// post: yields the old value, THEN steps; pre: steps first, yields the new one
println("reloadSeconds-- =", reloadSeconds--)
println("--reloadSeconds =", --reloadSeconds)
// compound assignment still requires the slot to already exist
try {
squad.readiness += 1
} catch (e) {
println("caught:", e)
}squad.strength = 14
reloadSeconds-- = 3
--reloadSeconds = 1
caught: the index 'readiness' (type='string') does not existOverloading an operator
A class defines a metamethod to give its instances behaviour for an operator. The name is the operator's metamethod from the table above, and it is an ordinary method on the class.
class MyPoint2 {
x = 0
y = 0
constructor(ax, ay) { this.x = ax; this.y = ay }
// getclass() rather than the class name: the name is not in scope inside the
// body, and this keeps working for a subclass
function _add(other) { return this.getclass()(this.x + other.x, this.y + other.y) }
function _sub(other) { return this.getclass()(this.x - other.x, this.y - other.y) }
function _mul(k) { return this.getclass()(this.x * k, this.y * k) }
function _unm() { return this.getclass()(-this.x, -this.y) }
// one _cmp drives <, <=, >, >= and == at once
function _cmp(other) { return (this.x * this.x + this.y * this.y)
<=> (other.x * other.x + other.y * other.y) }
function _tostring() { return $"({this.x}, {this.y})" }
function _typeof() { return "MyPoint2" }
}
let muzzle = MyPoint2(3, 4)
let recoil = MyPoint2(1, 2)
println("muzzle + recoil =", muzzle + recoil)
println("muzzle - recoil =", muzzle - recoil)
println("muzzle * 2 =", muzzle * 2)
println("-muzzle =", -muzzle)
println("muzzle > recoil =", muzzle > recoil)
println("muzzle == recoil =", muzzle == recoil)
println("typeof muzzle =", typeof muzzle)
println("type(muzzle) =", type(muzzle))muzzle + recoil = (4, 6)
muzzle - recoil = (2, 2)
muzzle * 2 = (6, 8)
-muzzle = (-3, -4)
muzzle > recoil = true
muzzle == recoil = false
typeof muzzle = MyPoint2
type(muzzle) = instanceThree things to watch in that class:
- A class cannot name itself inside its own body. The binding does not exist until the declaration finishes, so
MyPoint2(...)inside_addisUnknown variable [MyPoint2]. Usethis.getclass(). It also works when a subclass inherits the metamethod. A forward declaration works too when the name is needed. - One
_cmpdrives the ordering comparisons.<,<=,>and>=all go through it. Return a negative number, zero, or a positive one, as<=>does.==and!=are not among them. They compare raw identity, and no metamethod can change that. _typeofchangestypeof, not type.typeof muzzlegivesMyPoint2whiletype(muzzle)still givesinstance, becausetypeignores metamethods by design.
An operator with no metamethod on the class throws; there is no default fallback. An instance that defines _add but not _sub cannot be subtracted.
Metamethods gives the contract of each one: what it receives, which operand supplies it, and what the VM does with the returned value.
The static memoisation operator
static has two unrelated meanings. Inside a class body it declares a shared member, described in Classes and instances. In an expression it is the memoisation operator described here.
static expr evaluates expr the first time control reaches it and caches the result. Every later pass over the same place in the code reuses the cached value and evaluates nothing. It binds like the other unary operators (typeof, clone, unary -), so it takes the whole postfix expression after it.
Two consequences follow from "the same place in the code":
- The cache belongs to the code location, not to the call. A
staticinside a loop body evaluates on the first pass only, and every later iteration gets that first value. Two calls to the same function share one cache, because it is one location. Two identicalstaticexpressions written in two places have two separate caches. - A cached container is frozen. An
array,table,instance,classoruserdataresult is made immutable, as freeze does, so a later write throwstrying to modify immutable 'table'. This makes the shared value safe to hand out. A number, string or bool has nothing to freeze and is cached as is.
local built = 0
function palette() {
built++
return { sky = "#8ec5ff", ground = "#6b5a3e" }
}
function draw() {
let colors = static palette()
return colors.sky
}
println(draw())
println(draw())
println($"palette() ran {built} time(s)")
// The cached container is frozen, so no caller can corrupt it for the rest.
let colors = static palette()
try {
colors.sky = "#000000"
} catch (e) {
println($"write: {e}")
}
// One cache per code location, not per evaluation: the loop body caches on its
// first pass and reuses that value for every later one.
function tally() {
let before = built
for (local i = 0; i < 3; i++) {
let c = static palette()
c.sky
}
return built - before
}
println($"three passes through the loop called palette() {tally()} time(s)")#8ec5ff
#8ec5ff
palette() ran 1 time(s)
write: trying to modify immutable 'table'
three passes through the loop called palette() 1 time(s)Use it for work that is expensive and gives the same answer every time: a parsed table, a lookup built from constants, a colour palette. Do not use it for anything that depends on state the expression does not name, because nothing re-runs it when that state changes.
modules.reset_static_memos empties every cache in the VM. It is the only way to force re-evaluation. It exists for events like a script reload or a resolution change, and it is too expensive to call often.
Precedence traps
??binds looser than the comparison operators, soa ?? b > cparses asa ?? (b > c), not(a ?? b) > c.- Shift operators (
<< >> >>>) bind tighter than comparisons, so1 << 2 == 4parses as(1 << 2) == 4. &&binds tighter than||, as in most languages. Parenthesize a mixed chain when the reading is not obvious.- Assignment is a statement, not a nestable expression:
p = q = 5is a compile error ("'=' inside 'expression' is forbidden"). Write two statements instead.
let ammoLeft = 0
// trap: ?? binds LOOSER than comparisons, so this is ammoLeft ?? (ammoLeft > 0),
// not (ammoLeft ?? ammoLeft) > 0
println("ammoLeft ?? ammoLeft > 0 =", ammoLeft ?? ammoLeft > 0)
// trap: shifts bind TIGHTER than comparisons
println("1 << 2 == 4 =", 1 << 2 == 4)
// trap: && binds tighter than ||, so this is (squadReady && hasFuel) || override
let squadReady = true
let hasFuel = false
let override = true
println("squadReady && hasFuel || override =", squadReady && hasFuel || override)ammoLeft ?? ammoLeft > 0 = 0
1 << 2 == 4 = true
squadReady && hasFuel || override = truedelete
The delete operator is forbidden by default:
delete tbl.someKey
// Usage of 'delete' operator is forbidden. Use 'o.$rawdelete("key")' instead
Use rawdelete. It does the same removal as a regular method call and returns the removed value.
let activeMissions = { capture_flag = true, defend_base = true }
// the 'delete' operator is forbidden by default (see below); use rawdelete
println("activeMissions.rawdelete(\"capture_flag\") =", activeMissions.rawdelete("capture_flag"))
println("\"capture_flag\" in activeMissions =", "capture_flag" in activeMissions)
println("\"defend_base\" in activeMissions =", "defend_base" in activeMissions)activeMissions.rawdelete("capture_flag") = true
"capture_flag" in activeMissions = false
"defend_base" in activeMissions = trueSee Errors and exceptions for how a caller reacts to the errors shown above.