wowlua adds a few short forms to Lua 5.1 for code that addons write all the time. This page shows each one next to the Lua it compiles to, so you know what runs in the game.
The Lua on this page is real compiler output from a release build, unbundled and not minified. The syntax chapter of the specification has every rule.
Put values into strings
A string in backticks can hold any expression in {…}. You no longer need a
.. chain with tostring around each value.
local function announce(name: string, count: number, item: string) local msg = `{name} needs {count} more {item}{count == 1 and "" or "s"}` SendChatMessage(msg, "PARTY")end announce("Thrall", 2, "Linen Bandage")local function announce(name, count, item) local msg = tostring(name) .. " needs " .. tostring(count) .. " more " .. tostring(item) .. tostring(count == 1 and "" or "s") SendChatMessage(msg, "PARTY")end- A backtick string can span lines. The line breaks are part of the string.
\{and\}give literal braces, and\`a backtick.- Backtick strings also accept
\xFF,\u{2764}and\z. Quoted strings stay exactly as in Lua 5.1, where these escapes mean something else. - Constant values are folded into the text when the file is compiled:
literals,
constvalues, enum members and values fromwowlua:meta. Withaddonimported fromwowlua:meta,`{addon.title} loaded`becomes the plain string"MyAddon loaded". - There are no format specifiers. Use
string.formatfor%.1fand the like. - A backtick string is an expression, not a literal, so a call needs
parentheses around it:
print(`hi`), notprint`hi`.
Update a value in place
x += 1 means x = x + (1). Every arithmetic, concatenation and bitwise
operator has a compound form: += -= *= /= %= ^= ..= //= &=
|= <<= >>=.
local stats = { kills = 0 }local label = "Thrall"local counts: { [number]: number } = {} stats.kills += 1label ..= " (offline)"counts[C_Container.GetContainerItemID(0, 1)] -= 1stats.kills = stats.kills + 1label = label .. " (offline)"do local __wl_k1 = C_Container.GetContainerItemID(0, 1) counts[__wl_k1] = counts[__wl_k1] - 1endA call inside the target runs once, not twice. A compound assignment has one
target, so a, b += 1 is an error (E0111).
Fall back when something is nil
a ?? b is a, unless a is nil; then it is b. a or b would also
replace false. ?? keeps it, which matters for options a player can turn
off.
local db: { showMinimap: boolean? } = {}local cache: { [string]: number[] } = {} local showMinimap = db.showMinimap ?? true -- false stays falselocal title = GetGuildInfo("player") ?? "no guild" local function sample(unit: string) cache[unit] ??= {} -- create once, then reuse table.insert(cache[unit], UnitHealth(unit))end sample("player")print(showMinimap, title)local __wl_t1 = db.showMinimapif __wl_t1 == nil then __wl_t1 = trueendlocal showMinimap = __wl_t1 -- false stays falsebis only evaluated whenaisnil.x ??= vassigns only whenxisnil:if x == nil then x = v end.- Mixing
??withandororneeds parentheses (E0110), so the order is never a guess.
Reach into tables that might not be there
Optional chaining stops at the first nil and gives nil, where Lua would
raise "attempt to index a nil value".
interface Profile icon: number?end local db: { profiles: { [string]: Profile }? } = {} local function iconOf(name: string): number? return db.profiles?.[name]?.iconend print(iconOf("Default"))local function iconOf(name) do local __wl_t1 = db.profiles if __wl_t1 ~= nil then __wl_t1 = __wl_t1[name] if __wl_t1 ~= nil then __wl_t1 = __wl_t1.icon end end return __wl_t1 endendThere is a form for each kind of access:
local current = handle.profiles?:current() -- method callopts.onDone?.(result) -- call only if seta?.breads a field,a?.[k]indexes,a?:m()calls a method, andf?.()calls a function that may be missing.- Once a link is
nil, the rest of the chain is skipped, including the arguments of its calls. - Only
nilstops a chain.false?.xstill errors, as it does in Lua. - A chain that ends in a call gives exactly one value, like
(f()). - You can't assign through a chain:
a?.b = 1is an error (E0113).
Skip to the next loop iteration
continue jumps to the next iteration of the innermost loop. Like break, it
must be the last statement of its block (E0114). Write do continue end to
use it in the middle of one.
local function count(info: ContainerItemInfo) print(info.stackCount)end local function scan(bag: number) for slot = 1, C_Container.GetContainerNumSlots(bag) do local info = C_Container.GetContainerItemInfo(bag, slot) if not info then continue end if info.isLocked then break end count(info) endend scan(0)local function scan(bag) for slot = 1, C_Container.GetContainerNumSlots(bag) do local __wl_brk1 = false repeat local info = C_Container.GetContainerItemInfo(bag, slot) if not info then break end if info.isLocked then __wl_brk1 = true break end count(info) until true if __wl_brk1 then break end endendLua 5.1 has no goto, so the loop body is wrapped in a repeat … until true
that runs once, and continue becomes break. The flag only appears when a
loop uses both continue and break. continue is a keyword only at the
start of a statement that can't be anything else, so old code that uses it as
a variable name still works.
Name a value that never changes
const declares a local that can't be reassigned (E2010). When its value is
known at compile time, wowlua writes the value wherever you use the name, and
no local is emitted at all. That keeps you away from Lua's limit of 200
locals per function.
const ICON = 32const PADDING = 4const ROW = ICON + PADDING * 2const ROW_WIDTH = ROW * 6 local row = CreateFrame("Frame")row:SetSize(ROW_WIDTH, ROW)local row = CreateFrame("Frame")row:SetSize(240, 40)A value counts as known when it is a literal, another folded constant, a
string, number or boolean from wowlua:meta, or arithmetic, .., equality
and logic on those. Anything else becomes a normal local. A table bound with
const can still change inside; only the name is fixed.
Pull fields out of a table
local { a, b } = t declares one local per field and reads the field of the
same name. = value gives a default for a field that is nil.
local function addSpell(tooltip: GameTooltip, spellID: number) local info = C_Spell.GetSpellInfo(spellID) if not info then return end local { name, iconID, castTime = 0 } = info tooltip:AddLine(`|T{iconID}:0|t {name} ({castTime / 1000}s)`)end addSpell(GameTooltip, 6673)local name, iconID, castTime = info.name, info.iconID, info.castTimeif castTime == nil then castTime = 0endThe table is evaluated once. A local name is read directly, as here. Any other
expression, such as a call, is stored in a temporary first. Only local
destructures, and there is no array form: for several return values, Lua's
local a, b = f() already does the job.
Readable numbers and bit flags
Underscores separate digits, and 0b writes a binary number. Bitwise
operators and integer division compile to WoW's bit library and
math.floor.
const GOLD_CAP = 99_999_999_999const FLAG_DIRTY = 0b0001const FLAG_SHOWN = 0b0010 local flags = 0local items = {} flags |= FLAG_DIRTYlocal shown = flags & FLAG_SHOWN ~= 0local rows = #items // 6print(shown, rows, GOLD_CAP)flags = bit.bor(flags, 1)local shown = bit.band(flags, 2) ~= 0local rows = math.floor(#items / 6)&|~<<>>work on 32-bit integers, as WoW'sbitlibrary does, and need numbers.~xalone isbit.bnot(x);~=is still "not equal".>>is a logical shift (bit.rshift).a | b | cbecomes a singlebit.bor(a, b, c).//is floor division:7 // 2is3.- An underscore must sit between two digits:
1_000is fine,1_and0x_FFare errors (E0103).
Types on locals and loop variables
Any local, loop variable or parameter can have a type after a colon. Types are
checked, then erased: local count: number = 0 compiles to
local count = 0.
local free: number[] = {}for bag: number = 0, 4 do free[#free + 1] = C_Container.GetContainerNumFreeSlots(bag)endfor bag: number, slots: number in ipairs(free) do print(`bag {bag - 1}: {slots} free`)endvalue :: Type tells the checker what a value is when you know better than it
does. Both are covered in Types.
Settings for one file
A comment that starts with --! at the top of a file, before any code,
changes how wowlua treats that file. It is still a Lua comment, so the file
runs anywhere.
--!nocheck-- vendored code we do not want to touchlocal Parser = {}--!strictchecks the file in strict mode:nilmust be spelled out asT?, reading or writing an undeclared global is an error, and parameters of functions that aren't callbacks need types.--!basicis the default mode.--!nocheckskips type checking. The file is still parsed and compiled.--!nolintturns off lint warnings for the file.
The project-wide default is check.mode in wowlua.json, and a directive
overrides it for its file. Types explains the modes.
What stays out
WoW runs Lua 5.1, so some features of newer Lua versions are not part of wowlua:
gotoand labels.gotostays an ordinary name, as in Lua 5.1.- The
<const>and<close>attributes of Lua 5.4. Useconst. - A separate integer type. All numbers are doubles, as in the game.
\x,\u{…}and\zin quoted strings. Use a backtick string.