Type annotations
A name may carry : Type after it: a parameter, a return type after ):, a local or let declaration, a destructured field or element, or the vararg tail. This is core syntax and is always parsed. No flag turns it on or off in a script.
Syntax
All fourteen types from Values and types work as annotations. Three more exist only in annotations: number is short for int|float, any accepts every value and turns the check off for that parameter, and userpointer matches a raw pointer a native binding pushed. Combine types with |. Parentheses may group a union. A default value works together with a type; the common use is an optional nullable parameter, hp: int|null = null.
function damageAt(range: number, falloff: float = 0.5): float {
return 100.0 - range * falloff
}
println("damageAt(20) =", damageAt(20))
let describe = @(name: string, hp: int|null = null): string
hp == null ? name : $"{name} ({hp} hp)"
println("describe(\"scout\") =", describe("scout"))
println(describe("tank", 800))
function totalAmmo(...: int): int {
local sum = 0
foreach (n in vargv) sum += n
return sum
}
println("totalAmmo(30, 30, 12) =", totalAmmo(30, 30, 12))
let { weaponName: string, ammoLeft: int } = { weaponName = "mg42", ammoLeft = 120 }
println("weaponName, ammoLeft =", weaponName, ammoLeft)damageAt(20) = 90
describe("scout") = scout
tank (800 hp)
totalAmmo(30, 30, 12) = 72
weaponName, ammoLeft = mg42 120What the compiler does
An annotation becomes a check at the point it guards: a parameter is checked when its function is entered, a return value when the function returns, a declared or assigned variable when the write happens, and a destructured field or element when the destructuring runs.
When the type of the incoming value is not known until the check runs (a parameter, most assignments), the check is a runtime check, and a mismatch throws. When the type is known from the expression alone (a literal assigned directly), the compiler reports the mismatch at compile time:
function badReturn(): int {
return "not an int" // error: expression of type 'string' cannot be
} // assigned to type 'int', caught at compile time
function reload(ammoLeft: int) {
return ammoLeft - 1
}
try { reload("full") } catch (e) { println(e) }
function unpack(data) {
let { weaponName: string, ammoLeft: int } = data
return ammoLeft
}
let badLoadout = { weaponName = "mg42", ammoLeft = "full" }
try { unpack(badLoadout) } catch (e) { println(e) }parameter 1 of 'reload' has an invalid type 'string' ; expected: 'integer'
type 'string' differs from the declared type 'int'What it does not do
An annotation is a check, not a conversion. It never converts the value to the declared type. x: number accepts an int or a float as given, so a function that receives only ints returns an int. Quirrel has no function overloading, so an annotation cannot select between two bodies by argument type. There is one body for each function. The check runs only when a value arrives. An annotation does not prove that every caller passes the right type; it only shows that no caller so far has passed a wrong one.
function identity(x: number): number {
return x
}
println("type(identity(10)) =", type(identity(10))) // stays integer: the annotation only widens what is accepted
println("type(identity(10.0)) =", type(identity(10.0))) // stays float
function damageAt(range: number): float {
return 100.0 - range // the subtraction itself promotes the result to float
}
println("type(damageAt(10)) =", type(damageAt(10)))type(identity(10)) = integer
type(identity(10.0)) = float
type(damageAt(10)) = floatDeclaration strings
A native function has no Quirrel source to annotate. Its binding carries the same syntax as a string: pure type(obj): string, getbuildinfo(): table. There is one declaration string per native function. Every signature box on this site is rendered from these strings. sq --parse-types somefile.txt parses a file of such strings, one per line, and prints the result. This is how the string grammar is tested:
sq --parse-types decls.txt # decls.txt holds one declaration per line
pure clampAmmo(current: int, maxAmmo: int): int
pure clampAmmo(current: int, maxAmmo: int): int
functionName: clampAmmo
returnTypeMask: 0x2
objectTypeMask: 0xffffffff
ellipsisArgTypeMask: 0x0
requiredArgs: 2
argCount: 2
pure: true
nodiscard: false
--parse-types is a tool for that string grammar. It does not enable annotations in ordinary scripts; those are always parsed.