Values and types
Quirrel is dynamically typed: a variable has no type of its own; only the value in it has a type. type reports that type as one of fourteen strings.
The fourteen types
null, bool, integer, float, string, table, array, function, generator, userdata, thread, class, instance, weakref. Each name links to the methods that every value of that type has. A native closure reports function, the same as a script closure. A class's _typeof metamethod changes what the typeof operator reports, but never what type() reports (see below).
Truthiness
Only four values are false in a condition: null, false, the integer 0 and the float 0.0. Everything else is true. Unlike some other languages, an empty string, an empty array and an empty table are all true.
let checks = [
["null", null], ["false", false], ["0", 0], ["0.0", 0.0],
["empty string", ""], ["empty array", []], ["empty table", {}],
["nonempty string", "ready"], ["1", 1],
]
foreach (pair in checks) {
let label = pair[0]
let value = pair[1]
println($"{label}: {value ? "truthy" : "falsy"}")
}null: falsy
false: falsy
0: falsy
0.0: falsy
empty string: truthy
empty array: truthy
empty table: truthy
nonempty string: truthy
1: truthyIntegers and floats
An integer is 64 bit. A float in this build is single precision: getbuildinfo().floatsize is 4. The float size is a build option, not a language guarantee.
tofloat and tointeger convert explicitly. tointeger truncates toward zero; it does not round. Conversion of a large integer to float loses precision. Single precision keeps only 24 bits of mantissa, so an integer above 2^24 can round to a neighbor value. A mixed int == float comparison converts the integer with that same lossy cast before it compares, so a big integer can compare equal to a float that does not have its value.
from "debug" import getbuildinfo
let shellCount = 16777217 // 2^24 + 1
let asFloat = shellCount.tofloat() // single-precision float cannot hold it exactly
println("asFloat.tointeger() =", asFloat.tointeger()) // rounded down to 2^24
// comparing an int to a float promotes the int through the same lossy cast
println("shellCount == asFloat =", shellCount == asFloat)
println("getbuildinfo().floatsize =", getbuildinfo().floatsize) // 4: this build's float is single precisionasFloat.tointeger() = 16777216
shellCount == asFloat = true
getbuildinfo().floatsize = 4Equality and identity
== on two numbers compares by value, whether they are integers or floats. On two strings it compares by content. On two tables, arrays or instances it compares by identity: two separately built tables with the same content are not ==. Only a table compared with itself (or with an alias of it) is ==. null is its own type and equals only null; it is not equal to false or 0. <=> returns -1, 0 or 1 for ordering, and works on strings as well as numbers.
let squadA = { name = "alpha", strength = 4 }
let squadB = { name = "alpha", strength = 4 }
println("squadA == squadB =", squadA == squadB) // false: tables compare by identity, not content
println("squadA == squadA =", squadA == squadA) // true: same reference
println("1 == 1.0 =", 1 == 1.0) // true: int and float compare by numeric value
println("null == false =", null == false) // false: null equals only null
println("0 == false =", 0 == false) // false: no bool/number coercion in equality either
println("1 <=> 2 =", 1 <=> 2)
println("\"alpha\" <=> \"bravo\" =", "alpha" <=> "bravo")squadA == squadB = false
squadA == squadA = true
1 == 1.0 = true
null == false = false
0 == false = false
1 <=> 2 = -1
"alpha" <=> "bravo" = -1typeof, type, and classof
type is an ordinary function. It can be stored in a binding and passed to map like any other value. typeof is an operator, so let describe = typeof does not compile; it needs an operand. typeof(v) works, but the parentheses group v; they are not a call.
class Vehicle {
function _typeof() { return "Vehicle" }
}
let tank = Vehicle()
// typeof is an operator and consults _typeof; type is a function and does not
println($"{typeof tank} vs {type(tank)}")
// the parentheses people write are around the operand, not a call
println($"{typeof(tank)} is the same as {typeof tank}")
// being a real function value, type can be stored and passed
let describe = type
println("describe([1, 2]) =", describe([1, 2]))
println("types of [1, \"alpha\", 2.5] =", ", ".join([1, "alpha", 2.5].map(type)))
// typeof cannot: `let f = typeof` does not compile, it needs an operandVehicle vs instance
Vehicle is the same as Vehicle
describe([1, 2]) = array
types of [1, "alpha", 2.5] = integer, string, floatThe three ways to ask for a value's type:
- type
(v)is the plain engine category from the list above. It never consults a metamethod. typeof vis the same, unless the class ofvdefines_typeof. Then the result of that method is returned. Usetype()when the raw category matters.- classof
(v)returns the class object. For a scalar or container it is the built-in delegate (Integer,String, ..., imported from"types"). For a script class instance it is that class.
instanceof only walks the script class hierarchy, so instance instanceof the built-in Instance class is always false. To test whether a value is any class instance, use type(v) == "instance".
from "types" import classof, Integer, Instance
class Vehicle {
function _typeof() { return "Vehicle" }
}
let tank = Vehicle()
println("type(tank) =", type(tank)) // always the raw engine category
println("typeof tank =", typeof tank) // honors a class's own _typeof
println("classof(5) == Integer =", classof(5) == Integer) // classof(5) is the built-in Integer class
println("classof(tank) == Vehicle =", classof(tank) == Vehicle) // classof(instance) is its own script class
println("tank instanceof Instance =", tank instanceof Instance) // false: instanceof only walks the SCRIPT hierarchytype(tank) = instance
typeof tank = Vehicle
classof(5) == Integer = true
classof(tank) == Vehicle = true
tank instanceof Instance = false