luaguides

table.move

table.move(a1, f, e, t [,a2])

Signature and semantics

table.move copies e - f + 1 consecutive elements from the source table a1 into a destination table a2, starting at index t. The function returns a2, which makes the call chainable. In its most common form, the move is “in place” because the optional a2 defaults to a1.

table.move(a1, f, e, t [,a2])  -->  a2

Here is the form the Lua reference manual gives for table.move: the multiple assignment below. Read it that way and the function performs a single block copy, not a per-element loop. The move happens atomically, so callers never observe a half-finished state even when the source and destination ranges overlap.

a2[t], a2[t+1], ..., a2[t+(e-f)] = a1[f], a1[f+1], ..., a1[e]

That single line is the whole spec: read e - f + 1 elements from a1[f..e] and write them in order into a2[t..t+(e-f)]. If e < f, no elements are moved.

Parameters

ParamTypeDescription
a1tableSource table. Elements are read from a1[f..e].
fintegerSource start index (1-based). Must be at least 1.
eintegerSource end index, inclusive. Must be at least f.
tintegerDestination start index in a2. Any integer is allowed; a2 grows automatically.
a2table?Destination table. Defaults to a1 for an in-place move.

The destination range can overlap with the source range. The C code picks the correct direction (forward or backward) so the move behaves like memmove, not memcpy. This is why table.move exists: a hand-written for i=f,e do a2[t+i-f] = a1[i] end only gets the t < f direction right and silently clobbers source values when t > f.

Basic copy between two tables

The shortest useful call is a sequence-to-sequence copy:

local src = {10, 20, 30, 40, 50}
local dst = {}
table.move(src, 1, 5, 1, dst)
-- dst == {10, 20, 30, 40, 50}
-- table.move returns dst, so you can write:
-- local copy = table.move(src, 1, #src, 1, {})

Because the function returns a2, a one-liner shallow copy of a sequence is table.move(src, 1, #src, 1, {}). The destination does not need to be sized in advance: Lua tables grow as t increases past the current length.

In-place shift with overlapping ranges

When you omit a2, the source and destination are the same table. The function still produces the correct result when the ranges overlap, which a manual loop cannot do:

local t = {1, 2, 3, 4, 5}
table.move(t, 1, 2, #t)   -- move t[1..2] to t[4..5]
-- t == {1, 2, 3, 1, 2}

Here t[1..2] is the source range and t[4..5] is the destination range. The values 1 and 2 are read into temporary slots first, so neither is overwritten before it is consumed. Trying the same shift with for i=1,2 do t[3+i] = t[i] end corrupts the result because the second iteration reads the already-overwritten t[2].

Slicing a sub-range

Passing an explicit (f, e) pair is a clean way to extract a sub-array without computing #:

local buf = {"a", "b", "c", "d", "e"}
local window = {}
table.move(buf, 2, 4, 1, window)
-- window == {"b", "c", "d"}

This works equally well as the consumer side of a ring buffer: keep the live data in buf and copy the current window into a fresh window table on every tick.

Erasing a range with nil

Because reading a missing key yields nil, moving from a table that has nothing at the source indices clears the destination slots:

local t    = {1, 2, 3, 4, 5}
local nils = {}            -- #nils == 0
table.move(nils, 1, 3, 2, t)
-- t[2], t[3], t[4] are now nil
-- t == {1, nil, nil, nil, 5}

This is the cleanest stdlib-only way to nil out a contiguous range of a sequence.

Why not a for loop?

You can rewrite table.move as a for loop, and the result is identical for non-overlapping ranges. The difference shows up when the destination range sits inside or after the source range:

local t = {1, 2, 3, 4, 5}
table.move(t, 1, 3, 3)   -- move t[1..3] to t[3..5]
-- t == {1, 2, 1, 2, 3}

A naive forward loop breaks here. By the time the third pass reads from t[3], that slot already holds the 1 written on the first pass, so the original 3 is gone:

for i = 1, 3 do
  t[2 + i] = t[i]
end
-- t == {1, 2, 1, 2, 1}   -- the i=3 step reads t[3] after it was clobbered

table.move walks the ranges in the correct direction and gets the right answer. The C code picks forward or backward by comparing t and f, so you do not have to think about it.

A slice-and-join pattern

Once you have sliced a window out of a buffer, the next step is usually to do something with it. The natural follow-up is table.concat, which flattens a sequence of strings into one. The two together build a tiny read-only view without any helper library:

local buf = {"line 1\n", "line 2\n", "line 3\n", "line 4\n"}
local frame = {}
table.move(buf, 2, 3, 1, frame)
print(table.concat(frame))
-- line 2
-- line 3

This is the kind of pipeline where table.move pays for itself: the slice is a single C call, not a per-element loop, and the source buffer stays untouched for the next frame.

Gotchas

  • Lua 5.3 or later. table.move was introduced in Lua 5.3. It is not in Lua 5.1 or 5.2, and it is not in stock LuaJIT 2.0/2.1. The lua-compat-5.3 rock provides a polyfill for older interpreters.
  • Indices must be integers. Floats with integer values (2.0) work because Lua converts them. Non-integer floats and NaN raise an error.
  • No metamethods. The function treats indices positionally and does not invoke __index or __newindex. If a table has either metamethod, table.move skips them and reads or writes the raw slot.
  • # is not consulted. You pass e explicitly. For a sparse sequence, pass an e past the holes; using #t stops at the first hole.
  • Return value is the destination table, not a count. local n = table.move(t, 1, 3, 1) gives you t back, not the integer 3.
  • Forward and backward overlap both work. The C code picks direction by comparing t and f. Prefer table.move over a for loop whenever the ranges might overlap.

See also

  • table.insert — element-level append or insert; pair with table.move for bulk shifts.
  • table.remove — element-level delete; pair with table.move for splice operations.
  • table.concat — flatten a sequence of strings into one string; useful after slicing with table.move.
  • table.sort — in-place ordering, the same default-a2 mode table.move uses.
  • unpack — the inverse direction: spread a table into multiple return values.
  • Tables introduction — sequence basics that every table.move call assumes.
  • Arrays and lists — array semantics, copying, and slicing patterns.
  • Metatables guide — why table.move does not invoke __index or __newindex.