Polyglot
Languages Lua loops
Lua § loops

Loops

Lua admits four iteration forms: numeric for (for i = start, stop, step do), generic for (for var in iterator do), while (loop while truthy), and repeat ... until (the do-while analogue, with the condition checked at the end and until cond reading the loop’s locals). Lua does not admit a continue keyword — use goto (5.2+) or restructure. The combination — the four loop forms, the iterator protocol for generic-for, the absence of continue, the repeat...until distinctive scoping — is the substance of Lua’s iteration surface.

Numeric for

The numeric form:

for i = start, stop do
    -- body
end

for i = start, stop, step do
    -- body
end

Examples:

for i = 1, 10 do
    print(i)                                       -- 1 to 10
end

for i = 1, 10, 2 do
    print(i)                                       -- 1, 3, 5, 7, 9
end

for i = 10, 1, -1 do
    print(i)                                       -- 10 down to 1
end

for i = 0, 1, 0.1 do
    print(i)                                       -- 0, 0.1, 0.2, ... (float step)
end

The semantics:

  • The loop variable i is local to the loop.
  • The loop variable is not modifiable in the body (admitted but the loop sees the original value).
  • The start, stop, and step are evaluated once — admit substantial efficiency.

Generic for

The generic form admits iterator-driven iteration:

for var1, var2, ... in iterator_function do
    -- body
end

The conventional uses with pairs/ipairs:

local arr = {10, 20, 30}
for i, v in ipairs(arr) do
    print(i, v)
end

local map = {a = 1, b = 2}
for k, v in pairs(map) do
    print(k, v)
end

for word in string.gmatch("hello world", "%w+") do
    print(word)
end

The mechanism admits substantial custom iteration; treated in Iterators.

while

The conventional condition-driven loop:

while condition do
    -- body
end

Examples:

local n = 10
while n > 0 do
    print(n)
    n = n - 1
end

while not done do
    advance()
end

The condition is checked before each iteration; if initially false, the body never runs.

repeat ... until

The do-while analogue:

repeat
    -- body
until condition

The body runs at least once; the condition is checked at the end.

local input
repeat
    input = io.read()
until input == "quit"

-- Equivalent with while:
local input = io.read()
while input ~= "quit" do
    input = io.read()
end

A distinctive feature: the until sees the body’s locals:

repeat
    local doubled = compute()
until doubled > threshold                          -- doubled visible here!

The mechanism is unique to Lua among C-family languages — admits substantial “compute-then-check” patterns.

break

The break exits the innermost loop:

for i = 1, 100 do
    if i > 50 then break end
    if i % 2 == 0 then goto continue end           -- using goto for continue
    print(i)
    ::continue::
end

The break works in for, while, and repeat ... until.

goto and labels (Lua 5.2+)

Lua does not admit a continue keyword — the conventional substitute is goto:

for i = 1, 10 do
    if i % 2 == 0 then goto continue end
    print(i)                                       -- 1, 3, 5, 7, 9
    ::continue::
end

The ::label:: introduces a label; goto label jumps to it.

The mechanism admits substantial flexibility — particularly for “skip to next iteration” patterns. The conventional usage is forward-only jumps within a single function.

For Lua 5.1 (no goto), restructuring into helper functions is conventional:

-- 5.1 alternative:
for i = 1, 10 do
    if i % 2 ~= 0 then
        print(i)
    end
end

Or via inverted condition:

for i = 1, 10 do
    if not (i % 2 == 0) then
        print(i)
    end
end

Iterating tables

Sequence (ipairs)

local arr = {10, 20, 30, 40, 50}
for i, v in ipairs(arr) do
    print(i, v)
end

The ipairs iterates from 1 until the first nil.

All keys (pairs)

local map = {a = 1, b = 2, c = 3}
for k, v in pairs(map) do
    print(k, v)                                    -- order unspecified
end

Numeric for for arrays

local arr = {10, 20, 30}
for i = 1, #arr do
    print(i, arr[i])
end

The numeric form admits the same iteration as ipairs; the ipairs form is conventionally clearer.

Reverse iteration

local arr = {10, 20, 30}
for i = #arr, 1, -1 do
    print(arr[i])                                  -- 30, 20, 10
end

Sorted map iteration

local map = {c = 1, a = 2, b = 3}

local keys = {}
for k in pairs(map) do keys[#keys + 1] = k end
table.sort(keys)

for _, k in ipairs(keys) do
    print(k, map[k])                               -- "a 2", "b 3", "c 1"
end

The conventional pattern for deterministic map iteration.

Common patterns

Numeric range

for i = 1, 10 do print(i) end                      -- 1 to 10
for i = 1, 10, 2 do print(i) end                   -- 1, 3, 5, ...
for i = 10, 1, -1 do print(i) end                  -- 10 to 1

Iterate array

local arr = {"a", "b", "c"}
for i, v in ipairs(arr) do
    print(i, v)
end

-- Or with break:
for i, v in ipairs(arr) do
    if v == "stop" then break end
    print(v)
end

Iterate map

local config = {host = "localhost", port = 8080}
for k, v in pairs(config) do
    print(k, "=", v)
end

Filter via if/continue

for i, v in ipairs(items) do
    if not v.is_active then goto continue end
    if v.is_deleted then goto continue end
    process(v)
    ::continue::
end

-- Or restructure:
for i, v in ipairs(items) do
    if v.is_active and not v.is_deleted then
        process(v)
    end
end

The restructured form is conventionally clearer when the filter conditions are simple.

Counting

local count = 0
for i = 1, 1000 do
    if is_prime(i) then count = count + 1 end
end
print(count)                                       -- count of primes up to 1000

Sum

local sum = 0
for _, v in ipairs(arr) do
    sum = sum + v
end

Find

local function find(arr, target)
    for i, v in ipairs(arr) do
        if v == target then return i end
    end
    return nil
end

print(find({10, 20, 30}, 20))                      -- 2
print(find({10, 20, 30}, 99))                      -- nil

Map (transform)

local function map(arr, fn)
    local result = {}
    for i, v in ipairs(arr) do
        result[i] = fn(v)
    end
    return result
end

local doubled = map({1, 2, 3}, function(x) return x * 2 end)
-- {2, 4, 6}

Filter

local function filter(arr, pred)
    local result = {}
    for _, v in ipairs(arr) do
        if pred(v) then
            result[#result + 1] = v
        end
    end
    return result
end

local evens = filter({1, 2, 3, 4, 5}, function(x) return x % 2 == 0 end)
-- {2, 4}

Reduce

local function reduce(arr, init, fn)
    local acc = init
    for _, v in ipairs(arr) do
        acc = fn(acc, v)
    end
    return acc
end

local sum = reduce({1, 2, 3, 4}, 0, function(a, b) return a + b end)
-- 10

Polling loop

while not ready do
    check_status()
    -- sleep(1) (Lua does not include sleep; conventional via os.time)
end

Read lines

for line in io.lines("file.txt") do
    process(line)
end

-- From a file handle:
for line in file:lines() do
    process(line)
end

Generator-style (custom iterator)

local function range(start, stop, step)
    step = step or 1
    local i = start - step
    return function()
        i = i + step
        if (step > 0 and i <= stop) or (step < 0 and i >= stop) then
            return i
        end
    end
end

for i in range(1, 10) do print(i) end              -- 1 to 10
for i in range(10, 1, -2) do print(i) end          -- 10, 8, 6, 4, 2

Treated in Iterators.

Infinite loop with break

while true do
    local input = io.read()
    if input == nil or input == "quit" then break end
    process(input)
end

repeat...until for “do at least once”

local valid
repeat
    print("Enter a positive number:")
    local n = tonumber(io.read())
    valid = n and n > 0
    if valid then print("got", n) end
until valid

Loop with index and value

for i, v in ipairs(arr) do
    print(i, v)
end

-- Or with manual index:
local i = 0
for _, v in ipairs(arr) do
    i = i + 1
    print(i, v)
end

Skip first N

for i = N + 1, #arr do
    print(arr[i])
end

-- Or with skip via ipairs:
local i = 0
for _, v in ipairs(arr) do
    i = i + 1
    if i > N then
        print(v)
    end
end

Bounded iteration with break

for i, v in ipairs(arr) do
    if i > 10 then break end
    process(v)
end

Multiple iteration variables

-- Generic for with multi-value iterator:
for k, v, n in custom_iterator do
    -- ...
end

-- ipairs returns 2 values; pairs returns 2; gmatch may return more (for captures):
for whole, year in string.gmatch("date 2026 here 2027", "(%d+)") do
    -- whole and first capture
end

Nested loops

for i = 1, 10 do
    for j = 1, 10 do
        if i * j > 50 then
            break                                  -- breaks inner loop only
        end
        print(i, j)
    end
end

-- For multi-level break, use a flag or goto:
local done = false
for i = 1, 10 do
    if done then break end
    for j = 1, 10 do
        if condition then
            done = true
            break
        end
    end
end

-- Or with goto (5.2+):
for i = 1, 10 do
    for j = 1, 10 do
        if condition then
            goto outer_done
        end
    end
end
::outer_done::

Performance

For substantial loops, the conventional optimisations:

-- Cache table length (if not changing):
local len = #arr
for i = 1, len do
    process(arr[i])
end

-- Cache global as local:
local print = print
for i = 1, 1000000 do
    print(i)
end

-- Use ipairs for sequence; numeric for if you need start/end control:
for i, v in ipairs(arr) do
    -- substantial iteration
end

A note on the conventional discipline

The contemporary Lua loops advice:

  • Use numeric for for indexed iteration with explicit bounds.
  • Use generic for with ipairs for sequence iteration.
  • Use generic for with pairs for map iteration.
  • Use while for condition-driven loops.
  • Use repeat...until when the condition uses body-local values.
  • Use break freely.
  • Use goto continue (5.2+) for “skip to next” — or restructure.
  • Cache #t if not changing.
  • Use string.gmatch for tokenised iteration.
  • Use io.lines for file iteration.
  • Sort keys for deterministic map iteration.

The combination — numeric for, generic for with iterators, while, repeat...until with end-of-loop visibility, break, the goto/label mechanism (5.2+) — is the substance of Lua’s iteration surface. The discipline produces clear, explicit iteration code; the absence of continue admits substantial restructuring or goto-based workarounds.