Polyglot
Languages Lua syntax
Lua § syntax

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:

  • Falsynil and false.
  • 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 indexingt[1], not t[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 continue keyword — use goto or restructure.
  • No multi-line if-as-expression — use and/or or explicit if.
  • 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.