Polyglot
Languages Lua operators
Lua § operators

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):

OperatorMetamethod
+__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 or for 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.format for substantial formatting.
  • Use table.concat for 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.