Operators
Lua’s operator surface is small. The principal operators: arithmetic (+, -, *, /, //, %, ^, unary -); comparison (==, ~=, <, >, <=, >=); logical (and, or, not — admit substantial short-circuit and substitute for ternary); string concatenation (..); length (unary #); bitwise (&, |, ~, <<, >> — Lua 5.3+). Operators admit overloading via metatables — __add, __eq, __lt, __concat, etc. The combination — small operator set, distinctive ~= (not !=), the .. for concatenation, the # for length, the inequality difference, the metatable overloading — is the substance of Lua’s expression surface.
Arithmetic
a + b -- addition
a - b -- subtraction
a * b -- multiplication
a / b -- division (always float; 5.3+)
a // b -- floor division (5.3+)
a % b -- modulo
a ^ b -- exponentiation (always float)
-a -- unary negation
Examples:
print(7 / 2) -- 3.5 (always float)
print(7 // 2) -- 3 (integer floor division)
print(7 % 2) -- 1
print(2 ^ 10) -- 1024.0
print(-5) -- -5
The / is always float division (since 5.3); the // is floor division (integer for both integer operands).
For modulo, a % b == a - math.floor(a / b) * b:
print(7 % 2) -- 1
print(-7 % 2) -- 1 (sign follows divisor)
print(-7 % -2) -- -1
The mechanism differs from C-style modulo (where sign follows the dividend) — substantial for periodic computations.
Comparison
a == b -- equal
a ~= b -- not equal (note: ~= not !=)
a < b
a > b
a <= b
a >= b
The ~= is the inequality operator — distinct from != of C-family languages. The mechanism produces substantial confusion for newcomers from C.
The == requires type compatibility — comparing values of different types returns false:
print(1 == "1") -- false (different types)
print(1 == 1.0) -- true (number subtype OK)
print(nil == false) -- false
Tables, functions, userdata are compared by reference identity:
local t1 = {1, 2}
local t2 = {1, 2}
print(t1 == t2) -- false (different tables)
local t3 = t1
print(t1 == t3) -- true (same table)
For value equality on tables, the __eq metamethod admits overloading.
Logical
a and b -- AND (short-circuit)
a or b -- OR (short-circuit)
not a -- NOT
The principal distinction: and/or return one of their operands, not just true/false:
print(true and "yes") -- "yes" (returns second on truthy first)
print(false and "yes") -- false (returns first on falsy)
print(nil and "yes") -- nil
print("first" and "second") -- "second"
print(false or "default") -- "default" (returns second on falsy first)
print(nil or "default") -- "default"
print("first" or "second") -- "first" (returns first on truthy)
The and/or admit substantial conventional patterns:
Default-value pattern
local name = arg or "world" -- use arg, default "world"
local config = options or {} -- default empty table
Ternary-via-and-or
local max = (a > b) and a or b -- if a > b then a else b
local sign = (n < 0) and "negative" or (n > 0) and "positive" or "zero"
The pattern is the conventional substitute for the C-style ?:. The principal pitfall: if the “then” value is false or nil, the pattern returns the “else” value:
local x = (true) and false or "default" -- "default" (false treated as falsy!)
The defence is explicit if/else for cases involving false/nil:
local x
if condition then x = false else x = "default" end
Truthiness
Only nil and false are falsy:
if 0 then print("yes") end -- prints (0 is truthy)
if "" then print("yes") end -- prints
if {} then print("yes") end -- prints
if nil then print("yes") end -- nothing
if false then print("yes") end -- nothing
The strictness eliminates the C-family truthiness pitfalls.
String concatenation
The .. (two dots) admits string concatenation:
local s = "hello" .. " " .. "world" -- "hello world"
local s = "value: " .. 42 -- "value: 42" (number coerced)
local s = "pi is " .. 3.14
Numeric operands are implicitly converted via tostring for concatenation; non-numeric, non-string values require explicit conversion or __concat metamethod.
For substantial concatenation, table.concat is conventionally efficient:
-- Inefficient (each .. allocates):
local s = ""
for i, v in ipairs(items) do
s = s .. v
end
-- Efficient:
local s = table.concat(items, ", ")
-- For substantial dynamic building:
local parts = {}
for i, v in ipairs(items) do
parts[#parts + 1] = tostring(v)
end
local s = table.concat(parts, ", ")
The table.concat is O(n); the .. loop is O(n²).
Length operator #
The unary # returns the length of a string or array-style table:
print(#"hello") -- 5
print(#"") -- 0
print(#{10, 20, 30}) -- 3
print(#{}) -- 0
-- For tables, # returns a "border" — admits substantial implementation flexibility:
print(#{1, 2, nil, 4}) -- 2 or 4 (implementation-defined)
For map-style tables (with non-integer keys), # does not return the key count:
local t = {a = 1, b = 2, c = 3}
print(#t) -- 0 (no integer keys)
-- For map size:
local count = 0
for _ in pairs(t) do count = count + 1 end
print(count) -- 3
The conventional discipline is to use # only for sequence-style (integer-indexed, no nil holes) tables.
Bitwise operators (Lua 5.3+)
a & b -- AND
a | b -- OR
a ~ b -- XOR (yes, ~ not ^)
~a -- NOT (unary)
a << b -- left shift
a >> b -- right shift (logical for unsigned)
The ~ is both the binary XOR and the unary NOT — context-dependent.
print(0b1100 & 0b1010) -- 8 (0b1000)
print(0b1100 | 0b1010) -- 14 (0b1110)
print(0b1100 ~ 0b1010) -- 6 (0b0110)
print(~0b1010) -- -11
print(1 << 4) -- 16
print(255 >> 1) -- 127
The bitwise operators work on integers (5.3+); converted to integer for the operation.
For Lua 5.1/5.2 (and LuaJIT), the bit32 or bit libraries admit bitwise operations as functions:
-- LuaJIT:
local bit = require("bit")
bit.band(0b1100, 0b1010) -- 8
bit.bor(0b1100, 0b1010) -- 14
bit.bxor(0b1100, 0b1010) -- 6
Operator precedence
From highest to lowest:
| Operators |
|---|
^ (right-associative) |
not, #, unary -, ~ |
*, /, //, % |
+, - |
.. (right-associative) |
<<, >> |
& |
~ (binary) |
| ` |
<, >, <=, >=, ~=, == |
and |
or |
2 + 3 * 4 -- 14
(2 + 3) * 4 -- 20
2 ^ 3 ^ 2 -- 512 (right-assoc: 2^(3^2))
"a" .. "b" .. "c" -- "abc" (right-assoc)
not false and true -- true (not first)
The conventional discipline uses parentheses for clarity in mixed-precedence expressions.
Operator overloading
Operators admit overloading via metatables — special functions named __add, __sub, __mul, etc.:
local Vec = {}
Vec.__index = Vec
function Vec.new(x, y)
return setmetatable({x = x, y = y}, Vec)
end
Vec.__add = function(a, b)
return Vec.new(a.x + b.x, a.y + b.y)
end
Vec.__tostring = function(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local a = Vec.new(1, 2)
local b = Vec.new(3, 4)
local c = a + b -- (4, 6)
print(c) -- "(4, 6)"
The principal overloadable operators (metamethods):
| Operator | Metamethod |
|---|---|
+ | __add |
- (binary) | __sub |
* | __mul |
/ | __div |
// | __idiv |
% | __mod |
^ | __pow |
- (unary) | __unm |
.. | __concat |
# | __len |
== | __eq |
< | __lt |
<= | __le |
&, |, ~, <<, >>, ~ (unary) | __band, __bor, __bxor, __shl, __shr, __bnot |
Treated in Metatables.
Common patterns
Default value with or
local function fetch(url, options)
options = options or {}
local timeout = options.timeout or 30
local method = options.method or "GET"
-- ...
end
Ternary-via-and-or
local sign = (n > 0) and 1 or (n < 0) and -1 or 0
local message = active and "running" or "stopped"
local label = age >= 18 and "adult" or "minor"
The pattern admits substantial conciseness; the caveat is the false/nil “then” pitfall.
String concatenation
local greeting = "Hello, " .. name .. "!"
local description = string.format("%s is %d years old", name, age)
For substantial dynamic building, string.format is conventionally clearer.
Length checks
if #items == 0 then
print("no items")
end
local count = #items
for i = 1, count do
process(items[i])
end
Modulo for periodicity
for i = 0, 100 do
if i % 5 == 0 then
print(i) -- 0, 5, 10, ...
end
end
-- Wrap-around index:
local n = (i + 1) % len + 1 -- 1-based wrap
Bitwise flags
local READ = 1
local WRITE = 2
local EXECUTE = 4
local perms = READ | WRITE -- 3
print(perms & READ ~= 0) -- true
print(perms & EXECUTE ~= 0) -- false
perms = perms | EXECUTE -- add EXECUTE
perms = perms & ~READ -- remove READ
Comparison chain via and
-- Lua does not admit chained comparisons (a < b < c).
-- Conventional substitute:
if a < b and b < c then
print("ascending")
end
-- For range:
if 0 <= n and n < 100 then
print("in range")
end
Conditional assignment
-- Set if not nil:
config.timeout = config.timeout or DEFAULT_TIMEOUT
-- Conditional value:
local result = compute() or fallback
Truthiness with explicit nil check
-- Caveat: x might be valid false:
local val = x or default -- broken if x is false
-- Explicit:
local val
if x ~= nil then
val = x
else
val = default
end
-- Or with table-based default:
local options = options or {}
local timeout = options.timeout
if timeout == nil then timeout = 30 end
Floating-point equality
-- Avoid direct equality on floats:
if x == 0.1 then ... end -- may fail due to precision
-- Conventional defence:
local function near(a, b, eps)
eps = eps or 1e-9
return math.abs(a - b) < eps
end
if near(x, 0.1) then ... end
A note on ~= vs !=
The Lua inequality operator is ~=, not !=:
if a ~= b then ... end -- not equal
-- if a != b then ... end -- ERROR (not Lua syntax)
The choice is consistent with Lua’s ~ for bitwise XOR and NOT — the symbol is reused.
A note on the conventional discipline
The contemporary Lua operator advice:
- Use
==; remember~=(not!=). - Use
..for string concatenation. - Use
#for length (sequence-style tables). - Use
orfor default values (with caveats). - Use the and-or ternary for short value-returning conditionals.
- Use
//for integer division (5.3+). - Use
%for modulo (sign follows divisor). - Use
^for exponentiation (always float). - Use bitwise operators (5.3+) for substantial bit-twiddling.
- Use parentheses for clarity in mixed-precedence expressions.
- Use
string.formatfor substantial formatting. - Use
table.concatfor substantial concatenation in loops.
The combination — small operator set, the distinctive ~= and .., the truthiness of nil/false only, the or for defaults, the metatable-based overloading, the bitwise additions in 5.3 — is the substance of Lua’s expression surface. The discipline produces compact, readable code with substantial flexibility through metatable customisation.