Lua.VM.Table (Lua v1.0.0-rc.1)

View Source

Lua table data structure.

A single Elixir map backing both array and hash portions, plus a list of keys in insertion order and a key-set of "dead" keys whose values were cleared during iteration.

Keys and values are VM values (numbers, strings, booleans, {:tref, id}, etc.). Integer keys use 1-based indexing per Lua convention.

Dead-key tracking

Lua 5.3 §6.1 says iteration with pairs is well-defined when the body clears existing fields (t[k] = nil). The reference implementation preserves the iteration sequence by leaving cleared keys reachable in the hash chain (marked TDEADKEY) so the next call to next(t, k) can still find the entry that follows k.

We mirror that behavior with two pieces of state:

  • order — keys in the order they were first assigned a value. Live and dead keys both appear; assigning a fresh value to a previously dead key moves it to the end (it counts as a new insertion).
  • dead — a key => true map of keys that have been assigned nil. Their slot in order is preserved so next(t, k) can locate the slot, but data no longer contains the key, so the key is reported as absent to readers. (We use a plain map rather than MapSet here so dialyzer can treat the empty default opaquely; the API surface is the same — Map.has_key?/2, Map.put/3, Map.delete/2.)

All mutations should flow through put/3 (or put_data/3 for code that only has access to the underlying data map and doesn't care about the iteration ordering).

Summary

Functions

Flushes any pending appends in order_tail into order.

Builds a table struct from a plain data map.

Reads a value from a table data map, applying Lua key normalization (integer-valued floats collapse to integers per §3.4.11).

Returns true when the table data map has an entry for the given key after normalization.

Returns true if a key is invalid for use in a table assignment.

Returns the next key/value pair in iteration order after key.

Normalizes a table key per Lua 5.3 §3.4.11.

Writes value into the table under key, honoring Lua semantics

Writes value into a raw data map under key.

Applies an ordered list of {key, value} writes to the table, producing the same result as folding put/3 over the list left-to-right, but rebuilding the %Table{} struct only once at the end.

Replaces the data map wholesale, rebuilding order and clearing dead.

Types

t()

@type t() :: %Lua.VM.Table{
  data: %{optional(term()) => term()},
  dead: %{optional(term()) => true},
  metatable: {:tref, non_neg_integer()} | nil,
  order: [term()],
  order_tail: [term()]
}

Functions

flush_order(table)

@spec flush_order(t()) :: t()

Flushes any pending appends in order_tail into order.

Idempotent: a table with an empty tail is returned unchanged. Used by callers that want to amortize the cost of repeated next_entry calls (e.g. lua_next in the stdlib, which iterates via repeated calls).

from_data(data)

@spec from_data(map()) :: t()

Builds a table struct from a plain data map.

order is derived from the data map's key list — Erlang maps surface their keys in a deterministic order, so callers that pass us a literal data map (e.g. stdlib initialization) get a sensible iteration order with no extra effort.

get_data(data, key)

@spec get_data(map(), term()) :: term()

Reads a value from a table data map, applying Lua key normalization (integer-valued floats collapse to integers per §3.4.11).

has_data?(data, key)

@spec has_data?(map(), term()) :: boolean()

Returns true when the table data map has an entry for the given key after normalization.

invalid_key?(key)

@spec invalid_key?(term()) :: boolean()

Returns true if a key is invalid for use in a table assignment.

Per Lua 5.3 §3.4.11 / §6.1, table keys cannot be nil or NaN. Callers should raise with the appropriate "table index is nil" / "table index is NaN" error message before mutating table data.

next_entry(table, key)

@spec next_entry(t(), term()) :: {term(), term()} | nil | :invalid_key

Returns the next key/value pair in iteration order after key.

Walks the table's order list to find key, then advances through any dead-key slots until a live entry is found. Returns {k, v} for the next live entry, or nil when iteration is complete.

When key is nil, returns the first live entry (or nil if the table is empty/all-dead).

When key is non-nil and is not present in order at all, returns the sentinel :invalid_key so the caller can raise the user-facing "invalid key to 'next'" error per Lua 5.3 §6.1.

normalize_key(key)

@spec normalize_key(term()) :: term()

Normalizes a table key per Lua 5.3 §3.4.11.

Float keys that hold an exact integer value are coerced to integers so that t[1.0] and t[1] refer to the same slot. NaN keys are left as floats; callers that disallow NaN keys (set_table, rawset) detect the NaN and raise before reaching the data map.

put(table, key, value)

@spec put(t(), term(), term()) :: t()

Writes value into the table under key, honoring Lua semantics:

  • Assigning nil removes the key from data and marks it dead in order if it was previously live (Lua 5.3 §3.4.11 / §6.1).
  • Any other value is stored normally; if the key was previously dead, it is revived and re-appended to order so the new assignment counts as a fresh insertion.

Used by every code path that mutates table contents (set_table, set_field, set_list, rawset, table.insert, etc.) so the insertion-order invariant stays consistent.

put_data(data, key, value)

@spec put_data(map(), term(), term()) :: map()

Writes value into a raw data map under key.

Lower-level than put/3: operates only on the underlying map, with no awareness of order/dead. Use this when you have a data map but no surrounding Table struct (e.g. while folding through set_list intermediate state). Prefer put/3 whenever you have the full struct.

put_many(table, pairs)

@spec put_many(t(), [{term(), term()}]) :: t()

Applies an ordered list of {key, value} writes to the table, producing the same result as folding put/3 over the list left-to-right, but rebuilding the %Table{} struct only once at the end.

This is the batch entry point for table-constructor backfill (:set_list), where a run of consecutive integer keys is written in one go. The order/order_tail/dead invariants are maintained identically to repeated put/3: new keys append (via order_tail), existing live keys keep their position, dead keys revive to the end, and nil values clear a live key into dead. The win is collapsing count struct rebuilds into one.

pairs are applied in order; keys are normalized per normalize_key/1.

replace_data(table, data)

@spec replace_data(t(), map()) :: t()

Replaces the data map wholesale, rebuilding order and clearing dead.

Used by stdlib operations that rewrite the entire table contents (e.g. table.sort shuffles every integer key). After this call, iteration order reflects the new map layout.