Quirrel4.41.0

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.

examples/pages/language/annotations-syntax.nut
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)
Output:
damageAt(20) = 90
describe("scout") = scout
tank (800 hp)
totalAmmo(30, 30, 12) = 72
weaponName, ammoLeft = mg42 120

What 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
examples/pages/language/annotations-runtime-check.nut
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) }
Output:
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.

examples/pages/language/annotations-no-coercion.nut
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)))
Output:
type(identity(10)) = integer
type(identity(10.0)) = float
type(damageAt(10)) = float

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