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
| metamethod | runs when | this |
|---|---|---|
_get(key) | a read finds no slot | the object |
_set(key, value) | a write finds no slot | the object |
_newslot(key, value) | <- creates a slot | the object |
_delslot(key) | delete removes a slot | the object |
_nexti(previdx) | foreach asks for the next key | the object |
_add(other) | the + operator | the left operand |
_sub(other) | the - operator | the left operand |
_mul(other) | the * operator | the left operand |
_div(other) | the / operator | the left operand |
_modulo(other) | the % operator | the left operand |
_unm() | unary - | the operand |
_cmp(other) | < <= > >= and <=> | the left operand |
_call(callerThis, ...) | the object is called | the object |
_cloned(original) | clone has just copied it | the new object |
_typeof() | typeof | the object |
_tostring() | printing or string concatenation | the object |
_lock() | the class locks | the 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:
throw nullis a clean miss. The VM swallows it, continues the fallback chain, and finally raises its ownthe index 'x' (type='string') does not exist.- Any other throw is a real error. It reaches the caller wrapped as
Error in '_get' metamethod: <your value>. - Returning
nullis not a miss. It is the valuenull, successfully found.
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.
// 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}")
}hard
3
read: the index 'missing' (type='string') does not exist
write: Error in '_set' metamethod: config is read-onlyIterating
_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.
// _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}")4 4...
3 3...
2 2...
1 liftoffArithmetic
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.
// 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)Meters(10) + 2
Meters(10) - 2
int on the left: arith op - between 'integer' and 'instance'
Meters(10) + 2Comparison
_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:
==and!=do not use it. They compare raw identity, so two instances with the same contents are not equal, and no metamethod can change that. Compare a field, or write a named method.- Both sides must be the same type. An instance against an integer raises
comparison between instance and '0' (type='integer')without a call to_cmp.
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.
// _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))21
rifle
rifle, medkitSlot 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.