Lua to Lus
While Lus seems familiar to the seasoned Lua programmer, there are certain differences that you should be aware of while writing Lus code or adapting existing Lua code. Some of these differences can be safely ignored, while others may require partially rewriting or amending your code to work with Lus. Enabling -Wpedantic during development will catch many of these automatically.
This page is a concise migration reference. For full details on each feature, follow the links to the Guide and the relevant Acquis pages.
Use of -Wpedantic
Using the -Wpedantic flag during development can help catch old Lua idioms which are no longer relevant in Lus. This is your best friend for upgrading Lua code to Lus code; it will catch most of what can be safely rewritten by generating an AST for every chunk and analyzing them at compile time. That said, avoid using this flag in production as AST generation is processor-heavy and involves additional memory allocation.
lus -Wpedantic myscript.lus
See Acquis 14 for the full list of diagnostics produced by -Wpedantic.
Permissions (“pledging”)
Lus implements a capability-based permission model where scripts have to explicitly declare the access they need to the runtime and its libraries. While this can be used for sandboxing and other security purposes, its primary use case is to avoid unexpected behavior, such as unintended writes to sensitive system files. You will most likely have to pledge permissions to your scripts.
Pledging can be done either through the pledge function at runtime, or through --pledge/-P commandline arguments. When pledges are defined through commandline arguments, they are sealed by default and the interpreter cannot request additional pledges.
global pledge, print, fs
if pledge("fs") then
print(fs.list("."))
end
Permissions can be restricted with subpermissions (e.g. fs:read=/tmp/*), rejected with ~ prefixes, and sealed to prevent further changes. See Permissions in the Guide for the complete reference.
Error handling with catch
Instead of wrapping calls in pcall, Lus provides the catch keyword which works directly on any expression. The result follows the same ok, value convention as pcall.
-- In Lua, protected calls require wrapping in a function:
-- local ok, result = pcall(function() return riskyOp() end)
-- In Lus, catch works on any expression directly:
local ok, result = catch riskyOp()
-- With a handler to transform errors:
local ok, err = catch[simplify] riskyOp()
See Error handling, Catch handlers, and Acquis 1.
String interpolation
Backtick strings support interpolation with $ for identifiers and $(...) for expressions, replacing verbose concatenation.
-- Instead of concatenation:
-- print("Hello, " .. name .. "! Age: " .. tostring(age))
-- Backtick strings interpolate with $ and $():
print(`Hello, $name! Age: $age`)
print(`Result: $(x + y)`)
Optional chaining
The ? operator short-circuits to nil on any nil value in a chain, replacing verbose and-guard patterns.
-- Instead of chained and-checks:
-- local host = config and config.server and config.server.host
-- Use the ? operator:
local host = config?.server?.host
local name = getUser(id)?:getName()
local val = callback?()
See Optional chaining and Acquis 2.
If and while assignment
Variables can be declared directly in if and while conditions, scoped to the block. This replaces the pattern of pre-declaring a variable before testing it.
-- Instead of pre-declaring and testing:
-- local user = findUser(id)
-- if user then print(user.name) end
if user = findUser(id) then
print(user.name)
end
while line = file:read() do
process(line)
end
See if, elseif, else, while, Acquis 3, and Acquis 13.
Table destructuring
The from keyword extracts fields from a table into local variables in a single statement. It also works with require.
-- Instead of manually extracting fields:
-- local x = point.x; local y = point.y
local x, y from point
local insert, remove from require("table")
See Table destructuring and Acquis 4.
Slices
Tables and strings can be sliced with a two-argument index syntax, returning a new table or substring.
local t = {10, 20, 30, 40, 50}
local sub = t[2, 4] -- {20, 30, 40}
local s = "hello"[2, 4] -- "ell"
Table cloning
table.clone creates shallow or deep copies of tables, correctly handling circular references when deep copying.
local shallow = table.clone(original)
local deep = table.clone(original, true) -- circular refs preserved
See Table cloning and Acquis 17.
Do expressions
A do ... end block becomes an expression when it contains a provide statement, yielding a value from the block.
local result = do
local temp = compute()
provide temp * 2
end
See Do expressions and Acquis 23.
Enums
Lus has first-class enums. Each variant is a unique, opaque value that can be compared for equality but is distinct from strings and numbers.
local Color = enum red, green, blue end
print(Color.red) --> Enum<red>
print(Color.red == Color.red) --> true
print(Color.red == Color.green) --> false
Local groups
The <group> attribute creates a local variable whose table fields can be assigned individually while still being treated as a single binding.
local pos <group> = { x = 0, y = 0 }
pos.x = 10 -- ok
pos = { y = 5 } -- partial update
See Local groups and Acquis 18.
Variable attributes
Lus extends Lua 5.4’s <const> and <close> attributes and adds support for runtime attributes, which are user-defined functions called on every assignment to the variable.
local MAX <const> = 100 -- immutable
local f <close> = io.open(path) -- auto-closed on scope exit
-- Runtime attribute (called on every assignment):
local x <validator> = 10
See Variable attributes and Acquis 22.
Vectors
Vectors are mutable, fixed-size byte buffers for efficient binary data manipulation. They use string.pack/string.unpack format strings.
local v = vector.create(1024)
vector.pack(v, 0, "I4", 123456789)
local x = vector.unpack(v, 0, "I4")
New standard libraries
Lus adds several standard libraries not present in Lua.
JSON
The fromjson and tojson globals convert between Lus tables and JSON strings. No pledging is required.
local t = fromjson('{"key": "value"}')
local s = tojson(t)
Filesystem
The fs library provides file and directory operations. Requires pledge("fs").
local contents = fs.read("config.txt")
local entries = fs.list(".")
See Filesystem and Acquis 7.
Networking
The network library provides HTTP, TCP, and UDP support. Requires pledge("network").
local res = network.http("https://example.com")
See Networking and Acquis 9.
Transcoding
string.transcode converts strings between character encodings.
local utf16 = string.transcode(data, "utf-8", "utf-16")
See Acquis 8.
Workers
Workers provide true OS-thread concurrency via message passing, unlike coroutines which are cooperative and single-threaded.
local w = worker.create("task.lus", {data = "input"})
while result = worker.receive(w) do
print(result)
end
AST access
Lus exposes its parser for programmatic access. debug.parse(code) returns the AST as a Lus table. The CLI also provides --ast-json and --ast-graph flags for dumping the AST of a file.
lus --ast-json myscript.lus
lus --ast-graph myscript.lus
See Acquis 12.
Standalone binaries
Lus can bundle scripts and their dependencies into standalone executables with --standalone. Use --include to add additional files or directories to the bundle.
lus --standalone app.lus --include lib/
See Search paths and Acquis 19.