scripting / scripting-5Markdown ↓

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  --> 42

What 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 - discount

For an order of two items at 40 and one at 35, it returns 103.5. Line by line:

  • order is an input the app supplied; you just use it.
  • ipairs(order.lines) loops over the array order.lines in order. _ is the position (not needed here) and line is each element.
  • local makes a variable, and = assigns to it.
  • return hands 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  --> 1

Always 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          --> 3

Arrays 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  --> 2

Note that for i = 1, 5 includes 5: ranges include both ends.

Functions

local function area(w, h)
  return w * h
end

area(3, 4)  --> 12

A 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 = 2

There 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.5

Unions, 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" })  --> 5

The 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)     --> 2

Types 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 order above. 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 failed assert(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's try/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 pcall cannot catch it. That's deliberate: it keeps the app safe.

Gotchas cheat-sheet

The handful of things that surprise people coming from JavaScript:

  1. Arrays start at 1. items[1] is the first element; #items is the length.
  2. 0 and "" count as true. Only nil and false are false, so if 0 then runs.
  3. .. joins strings, not +. "a" + "b" is an error; "a" .. "b" is "ab".
  4. ~= means "not equal" (not !=), and it's and/or/not, not &&/||/!.
  5. No ++, += or var. Write x = x + 1, and always use local.
  6. Reading an unknown variable is an error, not undefined. That catches typos.
  7. nil is the only "empty". There's no separate null and undefined.
  8. One container type. {} is an object or an array depending on how you use it.
  9. Integer overflow is an error rather than wrapping round, so results stay correct.
  10. print may not exist, depending on the app. return is 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