Tables And Metatables
Tables are mutable identity-bearing maps. Keys and values are runtime values. Implementations may use optimized array, record, or typed layouts when this does not change observable behavior.
Constructors
Record/map constructors create fresh table identities. Named fields and
explicit keyed fields assign the specified key. Sequence values should use list
literals, which construct the same table value shape with consecutive 1-based
integer keys. Positional fields inside {...} are retained as compatibility
syntax, but new source should use [...] for lists and reserve {...} for
keyed records.
Portable programs must not depend on duplicate constructor fields that write the same normalized key; use explicit assignments when that order matters. A constructor expression itself never reuses an existing table identity.
```leia run all values := [“first”, “second”] user := {name: “Ada”}
assert(values[1] == “first”) assert(user.name == “Ada”) assert(user[“name”] == “Ada”)
```leia run all
a := ["x", "y"]
b := ["x", "y"]
assert(a != b)
assert(a[1] == "x")
assert(a[2] == "y")
a[3] = "third"
assert(a[3] == "third")
Assigning nil to a table field removes that field for ordinary table lookup.
Tables compare by identity unless a metatable supplies comparison behavior.
Raw Operations
Raw table operations are the primitive map operations:
rawget(t, k)returns the stored value for keyk, ornilif absent;rawset(t, k, v)storesvat keykand returnst;rawset(t, k, nil)removes keyk;rawequal(a, b)compares primitive equality without invoking__eq;rawlen(x)returns the primitive length for strings and tables.
Raw helpers bypass metamethods by contract. Non-raw operations may consult metatables when the corresponding operation names an event below.
```leia run all t := {} assert(rawset(t, “x”, 1) == t) assert(rawget(t, “x”) == 1) t.x = nil assert(rawget(t, “x”) == nil)
a := {} b := {} assert(rawequal(a, a)) assert(!rawequal(a, b))
```leia run all
log := []
t := setmetatable({present: 11}, {
__index: func(_, key) {
append(log, "index:" .. key)
return "fallback"
},
__newindex: func(_, key, value) {
append(log, "new:" .. key)
},
})
assert(t.present == 11)
assert(t.missing == "fallback")
assert(log[1] == "index:missing")
assert(rawget(t, "missing") == nil)
t.created = 12
assert(log[2] == "new:created")
assert(rawget(t, "created") == nil)
rawset(t, "created", 13)
assert(rawget(t, "created") == 13)
Metamethod Dispatch
Stable table/metamethod behavior is defined by the events below. A metamethod
is looked up by raw string key in the value’s metatable. The lookup never
invokes __index on the metatable itself. Each metamethod receives the listed
arguments; unless otherwise stated, only its first return value is used.
Only table values have user-controlled metatables in the v1.0 stable contract.
Strings have implementation-provided method lookup through __index, but
portable programs must not assume that setmetatable can change string
behavior.
For a binary event named event, Leia uses this lookup algorithm after any
primitive operation has failed:
- If the left operand is a table and its metatable has a non-
nilraw field namedevent, use that value. - Otherwise, if the right operand is a table and its metatable has a non-
nilraw field namedevent, use that value. - Otherwise, the operation raises the normal type error for that operator.
The chosen binary metamethod is called as (left, right) even when it came from
the right operand. Operators > and >= are normalized before lookup:
left > right dispatches as right < left, and left >= right dispatches as
right <= left. Leia does not require the two operands to share the same
metatable or metamethod function.
Stable Metamethods
| Metamethod | Trigger | Arity | Return contract |
|---|---|---|---|
__index |
t[k] or t.k when table t has no raw key k. Strings use an implementation-provided __index table for methods. |
If function: (t, k). If table: no call; lookup continues in that table with key k. |
Function result or redirected table lookup result. Missing chains produce nil; chains deeper than 50 redirects raise a runtime error. |
__newindex |
t[k] = v when table t has no raw key k. |
If function: (t, k, v). If table: no call; assignment continues in that table. |
Return values are ignored. Existing raw keys are updated directly. Chains deeper than 50 redirects raise a runtime error. |
__add |
left + right when primitive numeric addition is not applicable. |
(left, right) |
First return value is the operator result. |
__sub |
left - right when primitive numeric subtraction is not applicable. |
(left, right) |
First return value is the operator result. |
__mul |
left * right when primitive numeric multiplication is not applicable. |
(left, right) |
First return value is the operator result. |
__div |
left / right when primitive numeric division is not applicable. |
(left, right) |
First return value is the operator result. |
__mod |
left % right when primitive numeric modulo is not applicable. |
(left, right) |
First return value is the operator result. |
__pow |
left ** right when primitive numeric exponentiation is not applicable. |
(left, right) |
First return value is the operator result. |
__unm |
Unary -x when primitive numeric negation is not applicable. |
(x) |
First return value is the operator result. |
__eq |
left == right or left != right when both operands are tables and are not the same table identity. |
(left, right) |
Truthiness of the first return value determines equality; != negates it. Primitive values and same-identity tables use raw equality and do not call __eq. |
__lt |
left < right, or left > right after operand reversal. |
(left, right) for <; (right, left) for >. |
Truthiness of the first return value determines the comparison. |
__le |
left <= right, or left >= right after operand reversal. |
(left, right) for <=; (right, left) for >=. |
Truthiness of the first return value determines the comparison. There is no fallback to __lt; if neither operand supplies __le, the operation raises a comparison error. |
__concat |
left .. right when primitive string/number concatenation is not applicable. |
(left, right) |
First return value is the concatenation result. |
__len |
#x for a table x. |
(x) |
First return value is the length result. Library APIs that require an integer length may reject non-integer or negative results. |
__call |
Calling a non-function table value, t(...). |
(t, ...) |
All return values become the call result. |
__tostring |
tostring(x) for a table with this metamethod. |
(x) |
Must return a string; other results raise a runtime error. |
__metatable |
getmetatable(x) and setmetatable(x, mt) for a table whose current metatable has a non-nil raw __metatable field. |
Not called; read as a field. | getmetatable returns this value instead of the real metatable. Attempts to change the protected metatable raise a runtime error. |
Raw operations bypass metamethod dispatch by contract:
rawgetnever invokes__index;rawsetnever invokes__newindexand can create, replace, or remove raw keys directly;rawequalnever invokes__eq;rawlennever invokes__len.
__metatable protects the metatable slot, not arbitrary table contents. If a
program already holds a reference to the metatable table, ordinary writes to
that table remain ordinary table writes.
Implementation Extensions
The current implementation also supports __pairs for pairs(x) and __name
as a tostring fallback prefix. These are implementation extensions, not part
of the v1.0 stable table/metamethod contract above. Metamethods for integer
floor division, bitwise operators, finalizers, weak tables, binary chunks, and
Lua debug-slot protocols are not v1.0 stable contract unless a later spec
revision names them explicitly.
```leia run all vec := {x: 3} mt := { __add: func(a, b) { return {x: a.x + b.x} }, __eq: func(a, b) { return a.x == b.x }, __tostring: func(a) { return “vec(“ .. a.x .. “)” }, } setmetatable(vec, mt) other := setmetatable({x: 4}, mt) sum := vec + other assert(sum.x == 7) assert(vec == setmetatable({x: 3}, mt)) assert(tostring(vec) == “vec(3)”)
```leia run all
num_mt := {}
num := func(value) {
return setmetatable({value: value}, num_mt)
}
num_mt.__add = func(a, b) { return num(a.value + b.value) }
num_mt.__sub = func(a, b) { return num(a.value - b.value) }
num_mt.__mul = func(a, b) { return num(a.value * b.value) }
num_mt.__div = func(a, b) { return num(a.value / b.value) }
num_mt.__mod = func(a, b) { return num(a.value % b.value) }
num_mt.__pow = func(a, b) { return num(a.value ** b.value) }
num_mt.__unm = func(a) { return num(-a.value) }
num_mt.__concat = func(a, b) { return "num:" .. a.value .. ":" .. b.value }
a := num(5)
b := num(2)
assert((a + b).value == 7)
assert((a - b).value == 3)
assert((a * b).value == 10)
assert((a / b).value == 2.5)
assert((a % b).value == 1)
assert((a ** b).value == 25)
assert((-a).value == -5)
assert(a .. b == "num:5:2")
```leia run all callable := setmetatable({base: 10}, { __call: func(self, value) { return self.base + value, self.base - value }, })
sum, diff := callable(3) assert(sum == 13) assert(diff == 7)
mt := {__metatable: “locked”} protected := setmetatable({}, mt) assert(getmetatable(protected) == “locked”)
ok, err := pcall(func() { setmetatable(protected, nil) }) assert(!ok) assert(type(err) == “string”)
mt.__metatable = nil assert(setmetatable(protected, nil) == protected)
```leia run all
left := setmetatable({name: "left"}, {
__add: func(a, b) { return a.name .. "+" .. b.name },
})
right := setmetatable({name: "right"}, {
__add: func(a, b) { return a.name .. "->" .. b.name },
})
plain := {name: "plain"}
assert(left + right == "left+right")
assert(plain + right == "plain->right")
```leia run all mt := {} mt.__eq = func(a, b) { return a.key == b.key } mt.__lt = func(a, b) { return a.key < b.key }
a := setmetatable({key: 1}, mt) b := setmetatable({key: 1}, mt) c := a
assert(a == b) assert(rawequal(a, c)) assert(!rawequal(a, b)) assert(pcall(func() { return a <= b }) == false)
mt.__le = func(a, b) { return a.key <= b.key } assert(a <= b)
`__index` and `__newindex` table redirects are ordinary table operations on the
redirect target. They can chain through more metatables, and the same raw-key
rule is applied at each hop. A function-valued `__index` or `__newindex` stops
the chain by handling the operation directly.
```leia run all
base := {answer: 42}
middle := setmetatable({}, {__index: base})
obj := setmetatable({}, {__index: middle})
assert(obj.answer == 42)
log := []
sink := setmetatable({}, {
__newindex: func(_, key, value) {
append(log, key .. ":" .. value)
},
})
proxy := setmetatable({}, {__newindex: sink})
proxy.event = "saved"
assert(log[1] == "event:saved")
assert(rawget(proxy, "event") == nil)
```leia fail all t := {} setmetatable(t, {__index: t}) return t.missing
```leia fail all
t := {}
setmetatable(t, {__newindex: t})
t.missing = 1
Length And Sequence Traversal
The length operator #x uses the value’s ordinary length behavior and may
consult __len. rawlen(x) bypasses __len for tables and strings.
```leia run all t := setmetatable({}, { _len: func() { return 99 } }) assert(#t == 99) assert(rawlen(t) == 0)
Sequence length on sparse tables follows Leia runtime behavior. Programs that
depend on sparse length edge cases should pin that behavior with tests.
Iteration order for hash-like table keys is not a v1.0 stable contract.
Programs may depend on `pairs` visiting each key that remains present during an
ordinary traversal, but not on the order of those visits. `ipairs` is the
portable sequence traversal form for consecutive positive integer keys starting
at `1`; it stops at the first missing or `nil` element.
```leia run all
t := [10, 20]
t[4] = 40
t.name = "Ada"
seen := []
for _, value := range ipairs(t) {
append(seen, value)
}
assert(#seen == 2)
assert(seen[1] == 10)
assert(seen[2] == 20)
count := 0
for _ := range pairs(t) {
count = count + 1
}
assert(count == 4)