Quirrel4.41.0

Metamethods

A metamethod is an ordinary method with a reserved name. The VM calls it when an operation on the object has no built-in meaning: reading a slot that is not there, adding two instances, printing one. There are seventeen of them. Each has an exact contract: when it runs, what it receives, and what the VM does with the value it returns.

Where a metamethod can live

The VM looks for a metamethod in the object's delegate. For an instance the delegate is its class, so a method named _add in a class body is enough.

A table or a userdata can carry metamethods too, but only the host can set this up. The delegate is set through sq_setdelegate, and the script API has no equivalent. A table built in script has no delegate, so none of its metamethods can fire, regardless of slot names. Classes and instances are the carriers a script can build.

This matters most for _get: a plain table reports a missing key, and a _get slot in it is inert data.

All metamethods

metamethodruns whenthis
_get(key)a read finds no slotthe object
_set(key, value)a write finds no slotthe object
_newslot(key, value)<- creates a slotthe object
_delslot(key)delete removes a slotthe object
_nexti(previdx)foreach asks for the next keythe object
_add(other)the + operatorthe left operand
_sub(other)the - operatorthe left operand
_mul(other)the * operatorthe left operand
_div(other)the / operatorthe left operand
_modulo(other)the % operatorthe left operand
_unm()unary -the operand
_cmp(other)< <= > >= and <=>the left operand
_call(callerThis, ...)the object is calledthe object
_cloned(original)clone has just copied itthe new object
_typeof()typeofthe object
_tostring()printing or string concatenationthe object
_lock()the class locksthe class

== and != are not in that table; see Comparison below.

Reading and writing missing slots

_get and _set run only after the normal lookup has failed. They share one protocol to report that the key is absent:

So the return value cannot report an absent key, and a throw cannot report a present one. This lets a proxy distinguish a missing key from a key whose lookup failed.

examples/pages/language/metamethods-proxy.nut
// A read-only view over a config table. _get and _set decide what the dot means.
class Config {
  data = null
  constructor(data) { this.data = data }

  function _get(key) {
    if (key in this.data)
      return this.data[key]
    throw null          // clean miss: not an error, just "no such slot"
  }

  function _set(key, val) {
    throw "config is read-only"
  }
}

let cfg = Config({ difficulty = "hard", lives = 3 })
println(cfg.difficulty)
println(cfg.lives)

// throw null reaches the caller as the VM's own index error
try {
  println(cfg.missing)
} catch (e) {
  println($"read: {e}")
}

// any other throw reaches the caller unchanged
try {
  cfg.lives = 99
} catch (e) {
  println($"write: {e}")
}
Output:
hard
3
read: the index 'missing' (type='string') does not exist
write: Error in '_set' metamethod: config is read-only

Iterating

_nexti is asked for keys, not for values. The VM calls it with null on the first step and with the previously returned key after that. Returning null ends the loop. Each returned key is then read back through the ordinary read path, so an object with a _nexti almost always needs a matching _get. A key that cannot be read raises _nexti returned an invalid idx.

examples/pages/language/metamethods-nexti.nut
// _nexti hands out the next key; the VM then reads that key back through _get.
class Countdown {
  from = 0
  constructor(from) { this.from = from }

  function _nexti(previdx) {
    if (previdx == null)
      return this.from
    return previdx > 1 ? previdx - 1 : null   // null ends the loop
  }

  function _get(key) {
    if (typeof key == "integer" && key >= 1 && key <= this.from)
      return key == 1 ? "liftoff" : $"{key}..."
    throw null
  }
}

foreach (n, word in Countdown(4))
  println($"{n} {word}")
Output:
4 4...
3 3...
2 2...
1 liftoff

Arithmetic

The metamethod comes from the left operand and runs with it as this. There is no reversed form. If the left operand is an integer and the right is your instance, the integer decides, and the call fails with arith op - between 'integer' and 'instance'.

The one exception is an integer literal on the left of +. The compiler emits it as object + literal, because addition of numbers is commutative, so _add runs and sees the object as this. Do not rely on this to make a non-commutative operator work from either side.

An operator with no metamethod throws; there is no default fallback. An instance that defines _add but not _sub cannot be subtracted.

examples/pages/language/metamethods-operands.nut
// An arithmetic metamethod always comes from the left operand, and always runs
// with that operand as `this`.
class Meters {
  n = 0
  constructor(n) { this.n = n }
  function _add(other) { return $"Meters({this.n}) + {other}" }
  function _sub(other) { return $"Meters({this.n}) - {other}" }
}

let d = Meters(10)
let two = "2".tointeger()

println(d + two)
println(d - two)

// No metamethod on the left operand, so the integer decides, and an integer
// cannot add an instance.
try {
  println(two - d)
} catch (e) {
  println($"int on the left: {e}")
}

// The one exception: `literal + object` is compiled as `object + literal`, so
// _add still runs and still sees the object as `this`.
println(2 + d)
Output:
Meters(10) + 2
Meters(10) - 2
int on the left: arith op - between 'integer' and 'instance'
Meters(10) + 2

Comparison

_cmp returns an integer: negative if this sorts before other, zero if they are equal, positive if it sorts after. Any other return value raises _cmp must return an integer. It drives <, <=, >, >= and <=>, and sort uses it when no comparator is given.

Two limits:

The VM also short-circuits when both sides are the same object, so a comparison of an object with itself gives zero without a call.

Calling and cloning

_call makes the object callable. Its first parameter is not the first argument. It is the this of the call site, which the VM passes as it does for every call. The arguments follow it.

_cloned runs on the object that clone has already produced, with the original as its argument. Since clone is shallow, this is the place to give the copy its own nested containers.

examples/pages/language/metamethods-call-cloned.nut
// _call receives the call site's own `this` first, then the arguments.
class Multiplier {
  by = 0
  constructor(by) { this.by = by }
  function _call(originalThis, x) { return x * this.by }
}

let triple = Multiplier(3)
println(triple(7))

// _cloned runs on the new object, with the original as its argument, after the
// shallow copy is already made. It is the hook for deepening that copy.
class Loadout {
  items = null
  constructor() { this.items = ["rifle"] }
  function _cloned(original) {
    this.items = clone original.items    // otherwise both share one array
  }
}

let a = Loadout()
let b = clone a
b.items.append("medkit")
println(", ".join(a.items))
println(", ".join(b.items))
Output:
21
rifle
rifle, medkit

Slot creation and deletion

_newslot intercepts <-. On an instance it runs, but it cannot complete the job. Instance storage is a fixed-size block sized when the instance is built, so the slot still cannot appear. Use it to route the value somewhere else, such as a side table. On a table it runs only if the table has a delegate and the key is new; an assignment over an existing key is a plain write.

_delslot is unreachable from ordinary script. The delete operator is forbidden by default, and rawdelete, which the compiler suggests instead, is raw and skips metamethods. A host that clears that language feature gets delete back. A _delslot that runs is then responsible for the removal itself. The VM does not remove the slot, and the metamethod's return value becomes the value of the delete expression.

Type name and text

_typeof changes typeof, not type. typeof obj gives the string the metamethod returns, while type(obj) still gives "instance", because type ignores metamethods by design. See Values and types for when to use each of the two.

_tostring is used when the object is printed, concatenated into a string, or passed to sq_tostring from C. It must return a string. Without it, printing an instance gives an address.

_lock

_lock is the only metamethod that belongs to the class and not to its instances. It runs once, when the class locks: at its first instantiation, when another class inherits it, or on an explicit lock(), whichever happens first. this is the class object. The class can still be modified at that moment, so this is the last chance to add a member with newmember.

See Classes and instances for what locking prevents afterwards.