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
iis local to the loop. - The loop variable is not modifiable in the body (admitted but the loop sees the original value).
- The
start,stop, andstepare 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
forfor indexed iteration with explicit bounds. - Use generic
forwithipairsfor sequence iteration. - Use generic
forwithpairsfor map iteration. - Use
whilefor condition-driven loops. - Use
repeat...untilwhen the condition uses body-local values. - Use
breakfreely. - Use
goto continue(5.2+) for “skip to next” — or restructure. - Cache
#tif not changing. - Use
string.gmatchfor tokenised iteration. - Use
io.linesfor 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.