Syntax
Lua’s syntax is small and consistent — closer to Pascal than to the C family. The principal forms: function ... end for function declarations, if ... then ... elseif ... then ... else ... end for conditionals, while ... do ... end and for ... do ... end for loops, local for declaring local variables (variables without local are global), -- for comments. Statements are terminated by whitespace or newlines; semicolons are admitted but rarely used. Lua’s expression-vs-statement distinction is strict: most constructs are statements; expressions are restricted (no if-as-expression). The combination — keyword-delimited blocks, mandatory local for non-globals, expression-only function call syntax, the small core grammar — is the substance of Lua’s syntactic identity.
This page covers the surface a working programmer encounters routinely.
A complete program
The classical hello world:
print("Hello, world!")
A more substantial example:
local function greeting(name)
return "Hello, " .. name .. "."
end
local people = {"Alice", "Bob", "Charlie"}
table.sort(people)
for i, name in ipairs(people) do
print(greeting(name))
end
The principal features visible:
local function greeting(name)— function declaration (local).return ...— return statement."Hello, " .. name .. "."— string concatenation with...{"Alice", ...}— table constructor (used as array).table.sort(people)— standard-library function.for i, name in ipairs(people) do— generic-for loop.end— block terminator.
Execution:
lua hello.lua
For embedding into a host C/C++ application:
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaL_dofile(L, "hello.lua");
lua_close(L);
The mechanism admits substantial scripting integration; treated in Modules and packaging.
Source character set
Lua source is interpreted as a byte sequence (typically ASCII or UTF-8). String literals admit any bytes; identifiers are restricted to ASCII letters, digits, and underscores.
Identifiers and keywords
Identifiers begin with a letter or underscore and continue with letters, digits, or underscores. Lua is case-sensitive: count and Count are distinct identifiers.
Convention follows the Lua community style:
lower_snake_case— variables, functions, methods (the conventional default).PascalCase— sometimes used for classes/modules (rare).UPPER_CASE— constants (by convention; not language-enforced).- Leading underscore — for “private” (not enforced).
_VERSION— pre-defined globals begin with_.
The 22 reserved keywords (Lua 5.4):
and break do else elseif
end false for function goto
if in local nil not
or repeat return then true
until while
The keywords cannot be used as identifiers.
Comments
Two principal forms:
-- A single-line comment, terminated by the end of the line.
--[[
A multi-line comment.
Bracketed by --[[ and ]].
]]
--[==[
A long comment that admits ]] inside,
delimited by --[==[ and ]==].
]==]
The --[[ ... ]] is a long bracket; the == between brackets admits substantial nesting (any number of = signs, matching count required to close).
The conventional discipline uses -- for ordinary comments; --[[ ... ]] for substantial multi-line documentation or temporarily-disabled code.
Statement terminators
Newlines and whitespace terminate statements; semicolons admit multiple statements on one line:
local x = 5
local y = 10
local z = x + y
-- Equivalent:
local x = 5; local y = 10; local z = x + y
The conventional discipline omits semicolons.
A subtle pitfall: two statements on consecutive lines may be ambiguous when one ends with a parenthesised expression and the next begins with (. The parser may interpret as a single function call:
local a = b + c
(d)() -- parsed as: local a = b + c(d)()
The conventional defence is a leading semicolon:
local a = b + c
;(d)() -- explicit statement separator
The form is rare; the conventional code structure rarely produces this issue.
Variable declarations
The local keyword introduces a local (block-scoped) variable:
local x = 5
local name = "Alice"
local count, total = 0, 0
Variables without local are global:
x = 5 -- creates global x (or assigns existing)
y = 10 -- global y
The conventional discipline uses local for almost everything — globals are conventionally avoided except for module exports.
For constants (Lua 5.4+):
local MAX_RETRIES <const> = 3 -- compile-time constant
local CONFIG <const> = { ... }
The <const> admits compile-time enforcement of immutability.
For to-be-closed variables (Lua 5.4+):
local file <close> = io.open("data.txt", "r")
-- file:close() called automatically when scope exits
The <close> admits substantial RAII-style resource management.
Multiple assignment
Lua admits multiple assignment in a single statement:
local a, b = 1, 2
a, b = b, a -- swap
local x, y, z = 10, 20, 30
-- From function returns:
local quotient, remainder = divmod(17, 5)
If the right side has fewer values, the rest are nil:
local a, b, c = 1, 2 -- a=1, b=2, c=nil
If the right side has more values, the extras are discarded:
local a, b = 1, 2, 3 -- a=1, b=2, 3 discarded
Functions
The function keyword introduces a function:
function greet(name)
return "Hello, " .. name
end
local function add(a, b)
return a + b
end
-- As a value:
local square = function(n)
return n * n
end
The local function form declares a local function. The function name(...) end form declares a global function (assigns to the name).
For table-method syntax:
local Person = {}
function Person.greet(name) -- dot-form: regular function
return "Hello, " .. name
end
function Person:greet(other) -- colon-form: method (implicit self)
return self.name .. " greets " .. other
end
The colon syntax (Person:greet(...)) admits implicit self parameter. Treated in Functions and closures and OOP idioms.
Block syntax
All control-flow constructs use keyword-delimited blocks:
if condition then
-- body
elseif other then
-- body
else
-- body
end
while condition do
-- body
end
for i = 1, 10 do
-- body
end
repeat
-- body
until condition
function name(params)
-- body
end
do
-- explicit block (rare; for scoping)
end
The end keyword terminates each block. There are no braces.
Statement vs expression
Lua maintains a strict distinction between statements and expressions:
- Statements — declarations, assignments, control flow, function calls (when used for side effects),
return,break. - Expressions — literals, arithmetic, comparisons, function calls (when used for the return value).
Conditionals and loops are statements — they do not return values:
-- Cannot:
-- local max = if a > b then a else b end -- ERROR
-- Conventional substitute (the and-or trick):
local max = a > b and a or b
-- Or with explicit if/else:
local max
if a > b then max = a else max = b end
The and/or form is conventional for ternary-like expressions, with the caveat that false/nil results break the pattern (treated in Operators).
do ... end
The do ... end admits explicit block-scoped declarations:
do
local temp = compute()
process(temp)
end
-- temp not visible here
The form is rarely used — most blocks come from control-flow constructs.
Truthiness
Lua’s truthiness is strict:
- Falsy —
nilandfalse. - Truthy — everything else, including
0,"",{}.
if 0 then print("yes") end -- prints; 0 is truthy
if "" then print("yes") end -- prints; "" is truthy
if {} then print("yes") end -- prints; {} is truthy
if nil then print("yes") end -- nothing
if false then print("yes") end -- nothing
The strictness eliminates the C-family truthiness pitfalls.
A note on what Lua admits
Several distinguishing features:
- Tables — the single composite type; admits arrays, hashes, objects, modules.
- First-class functions — admit closures, anonymous, multiple returns.
- Coroutines — built-in cooperative multitasking.
- Metatables — admit operator overloading and OOP via
__index. - Pattern matching — Lua’s patterns (not regex) for substantial string manipulation.
- Multiple return values — first-class.
- Varargs —
...admits variadic functions. - 1-based indexing —
t[1], nott[0]. - Garbage collection — incremental tracing GC (5.4+ admits generational mode).
- Embedding — designed for use as a guest in C/C++ host applications.
A note on what is not in Lua
- No classes — OOP via tables and metatables.
- No declared types — dynamic typing throughout.
- No
continuekeyword — usegotoor restructure. - No multi-line
if-as-expression — useand/oror explicitif. - No bitwise operators until Lua 5.3.
- No integer type until Lua 5.3 (everything was a float).
- No standard regex — Lua’s patterns are simpler.
- No package manager in the language — LuaRocks is the conventional contemporary tool.
The combination — small grammar, single composite type, mandatory local, the distinctive 1-based indexing, the table-centric design, the substantial embedding model — is the substance of Lua’s identity. The discipline produces clear, compact code with substantial flexibility for embedding scenarios.