Getting started with SwiftLua
This is a gentle, hands-on introduction to writing SwiftLua scripts. You don't need to know Lua. If you've written a little JavaScript or TypeScript, you already know most of the ideas, and this guide points out the few differences as it goes. When you want the precise rules for anything here, the Language Guide is the full reference.
In the examples, --> shows what a line produces:
local answer = 6 * 7
answer --> 42What you're writing
A SwiftLua script is a small program that runs inside an app, safely. The app gives
your script some inputs, your script works something out, and it hands back a result
with return. A script can't reach files, the network or the clock unless the app gives
it a function for that, and it always finishes, because the app sets time and memory
limits.
Think of a script as a function the app calls: inputs in, a value out.
If you've written JavaScript or TypeScript
Most of your instincts carry over:
| You know (JS/TS) | In SwiftLua |
|---|---|
let x = 1 / const x = 1 |
local x = 1 (always local; there's no var) |
// comment |
-- comment |
=== / !== |
== / ~= (note: ~=, not !=) |
&& / || / ! |
and / or / not |
a + b to join strings |
a .. b (.. joins; + is for numbers only) |
null / undefined |
nil (just one) |
obj.field, arr[0] |
obj.field, arr[1] (arrays start at 1) |
objects {} and arrays [] |
both are tables {} (one type, two uses) |
x: number, "a" | "b", T | null |
x: number, "a" | "b", T?: TypeScript-style types, checked before the script runs |
try { } catch { } |
pcall(...) (covered below) |
The differences that trip people up are collected in the cheat-sheet at the end. Skim it once and you'll avoid the common surprises.
Your first script
Say the app runs your script with an order input and wants the total price, with a 10%
discount once the subtotal passes 100. Here's the whole thing:
local subtotal = 0
for _, line in ipairs(order.lines) do
subtotal = subtotal + line.qty * line.price
end
local discount = 0
if subtotal > 100 then
discount = subtotal * 0.1
end
return subtotal - discountFor an order of two items at 40 and one at 35, it returns 103.5. Line by line:
orderis an input the app supplied; you just use it.ipairs(order.lines)loops over the arrayorder.linesin order._is the position (not needed here) andlineis each element.localmakes a variable, and=assigns to it.returnhands your result back to the app.
That's a real script. Everything below is more building blocks.
The building blocks
Variables
local total = 0
local name, qty = "widget", 3 -- declare several at once
total = total + 1 -- assign (there is no ++ or +=)
total --> 1Always use local. Assigning to a name you never declared (total = 1 with no local)
creates a global, which the checker reports as an error, because it's almost always a
typo. Reading a name that doesn't exist is an error too, where JavaScript would quietly
give you undefined.
Numbers and strings
local n = 10
local r = 10 / 4
r --> 2.5
local s = "qty=" .. n
s --> "qty=10"/ always gives a decimal result, and .. turns numbers into text for you. Integers and
decimals are distinct, and integer arithmetic that overflows stops with an error
instead of silently wrapping round. You'll rarely hit it, but it's why results can be
trusted.
Tables: objects and arrays
A table is the only container. It's both your "object" and your "array":
local person = { name = "Ada", age = 36 } -- like a JS object
person.name --> "Ada"
person["age"] --> 36
local items = { "a", "b", "c" } -- like a JS array
items[1] --> "a"
#items --> 3Arrays start at 1, not 0, and # gives the length. You can mix the two uses in one table,
but typed code is clearer when arrays and records stay separate.
Conditionals and loops
local score = 85
local grade
if score > 90 then
grade = "A"
elseif score > 80 then
grade = "B"
else
grade = "C"
end
grade --> "B"
local sum = 0
for i = 1, 5 do sum = sum + i end -- 1 through 5, inclusive
sum --> 15
local letters = ""
for _, v in ipairs({ "x", "y", "z" }) do -- each array element, in order
letters = letters .. v
end
letters --> "xyz"
local fields = 0
for key, value in pairs({ name = "Ada", age = 36 }) do -- every key and value
fields = fields + 1
end
fields --> 2Note that for i = 1, 5 includes 5: ranges include both ends.
Functions
local function area(w, h)
return w * h
end
area(3, 4) --> 12A function can return more than one value. It's handy for "a result and an error", and reads like destructuring a tuple in TypeScript:
local function divmod(a, b)
return a // b, a % b -- // is whole-number division
end
local q, r = divmod(17, 5) --> q = 3, r = 2There are a few precise rules about multiple values (Language Guide §4), but the common case is exactly this.
Types: like TypeScript, checked before the script runs
You can annotate variables, parameters and return types, and a checker verifies them before the script runs, so typos and mismatched types are caught up front rather than halfway through.
local total: number = 0
local names: {string} = {} -- array of strings
local rates: {[string]: number} = { GBP = 1.25 } -- map: string keys to numbers
local status: "open" | "closed" = "open" -- only these two values allowed
local note: string? = nil -- optional: a string or nil
local function convert(amount: number, code: string): number
return amount * (rates[code] or 1)
end
convert(10, "GBP") --> 12.5Unions, literal types, optionals (T?), arrays ({T}), maps ({[K]: V}) and function
types ((number) -> number) are all there. You can name your own shapes too:
type Line = { sku: string, qty: number }
type Order = { lines: {Line}, status: "open" | "closed" }
local function units(order: Order): number
local count = 0
for _, line in ipairs(order.lines) do count = count + line.qty end
return count
end
units({ lines = { { sku = "A", qty = 2 }, { sku = "B", qty = 3 } }, status = "open" }) --> 5The checker also narrows types as your code tests them, just like TypeScript:
local function describe(value: number | string): string | number
if type(value) == "string" then
return value .. "!" -- here the checker knows value is a string
else
return value + 1 -- and here it's a number
end
end
describe("hi") --> "hi!"
describe(1) --> 2Types are optional: start without them and add them as you go. The checker is strict by
default; putting --!nonstrict on the first line turns type findings into warnings while
you're learning (Language Guide §9).
Working with the app
Your script reaches the outside world only through what the app provides. Which inputs, functions and modules exist depends on the app, and its own documentation lists them. The names below are examples.
- Inputs and data: values the app sets, like
orderabove. Just read them. - Functions the app gives you, sometimes grouped under a name, such as
orders.fetch(1042). You call them like any other function. Some do real work (reading a database, calling a web service) and wait briefly while they do; that happens by itself, with no special syntax (Language Guide §12). - Modules:
local helpers = require("helpers")loads a script the app makes available, usually a table of functions. - Your result: whatever you
return.
A function from the app might return a value and an error, which you read as a pair:
local qty, err = parseQuantity("12")
qty --> 12
err --> nil
local bad, problem = parseQuantity("twelve")
bad --> nil
problem --> "not a quantity"When things go wrong
Errors stop the script:
error("bad input"), a failedassert(condition, "message"), or a mistake at run time such as calling something that isn't a function. The app receives a clear error, with the line it happened on.Catching errors uses
pcall, SwiftLua'stry/catch:local function risky() error("out of stock", 0) end local ok, result = pcall(risky) ok --> false result --> "out of stock"Limits are set by the app: a script can't loop forever or use unlimited memory. If it reaches a limit, the run ends with an error saying which one, and
pcallcannot catch it. That's deliberate: it keeps the app safe.
Gotchas cheat-sheet
The handful of things that surprise people coming from JavaScript:
- Arrays start at
1.items[1]is the first element;#itemsis the length. 0and""count as true. Onlynilandfalseare false, soif 0 thenruns...joins strings, not+."a" + "b"is an error;"a" .. "b"is"ab".~=means "not equal" (not!=), and it'sand/or/not, not&&/||/!.- No
++,+=orvar. Writex = x + 1, and always uselocal. - Reading an unknown variable is an error, not
undefined. That catches typos. nilis the only "empty". There's no separatenullandundefined.- One container type.
{}is an object or an array depending on how you use it. - Integer overflow is an error rather than wrapping round, so results stay correct.
printmay not exist, depending on the app.returnis how you give back a result.
Where to go next
The Language Guide is the full reference. Useful places to jump to:
- Multiple return values, the exact rules: §4
- Tables, iteration order and the class pattern: §5
- Modules and
require: §6 - The standard library (math, string, table and set helpers, time, UUIDs): §8
- The extra libraries (json, hash, url, csv, decimal, stats, template and more), where the app offers them: Extra libraries
- The type system in depth (enums, exhaustiveness, narrowing): §9
- Errors and limits: §10
- Objects the app provides: §11
- Functions that wait on the outside world: §12
You don't need all of it to be productive. Come back when a specific question comes up. Happy scripting.
Published platform manual · revision 5866bdb919fb