Quirrel4.41.0

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.

examples/pages/language/types-truthiness.nut
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"}")
}
Output:
null: falsy
false: falsy
0: falsy
0.0: falsy
empty string: truthy
empty array: truthy
empty table: truthy
nonempty string: truthy
1: truthy

Integers 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.

examples/pages/language/types-precision.nut
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 precision
Output:
asFloat.tointeger() = 16777216
shellCount == asFloat = true
getbuildinfo().floatsize = 4

Equality 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.

examples/pages/language/types-equality.nut
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")
Output:
squadA == squadB = false
squadA == squadA = true
1 == 1.0 = true
null == false = false
0 == false = false
1 <=> 2 = -1
"alpha" <=> "bravo" = -1

typeof, 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.

examples/pages/language/types-type-vs-typeof.nut
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 operand
Output:
Vehicle vs instance
Vehicle is the same as Vehicle
describe([1, 2]) = array
types of [1, "alpha", 2.5] = integer, string, float

The three ways to ask for a value's type:

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".

examples/pages/language/types-typeof-classof.nut
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 hierarchy
Output:
type(tank) = instance
typeof tank = Vehicle
classof(5) == Integer = true
classof(tank) == Vehicle = true
tank instanceof Instance = false