Brass for Lua and ComputerCraft users
Every difference with Lua 5.x and with ComputerCraft's APIs, why it exists, and what to write instead.
If you know Lua, or have programmed ComputerCraft computers, you already know most of Brass: the same keywords, the same tables, local, pairs, .., string.format. This page lists everything that differs, so that your habits work for you instead of against you. A side-by-side table closes the page.
Most differences have one of two reasons. Brass runs on its own small virtual machine inside the server, which counts every instruction and every cell of memory of each computer: no program can ever lag the server or eat its memory. And Brass is meant to be learned in game, so a few traps of Lua were removed or turned into clear errors.
What stays the same
- The syntax:
local,function,if ... elseif ... else ... end,while,repeat ... until, numericfor,for k, v in pairs(t),break,do ... end,return. - Comments
--and--[[ ... ]], strings in"...",'...'and[[ ... ]]. - Tables indexed from 1,
#t,t.nameandt[key],{1, 2, x = 3}. and,or,not, with the same "onlynilandfalseare false" rule (so0and""are true).//(floor division) and%as in Lua 5.3,^for powers.- Method calls
obj:method(x)andfunction obj:method(x)withself, call sugarf "text"andf {table}. tostring,tonumber,type,pairs,ipairs,error,assert, and the usual functions ofstring,tableandmath.
The language
No closures over the locals of a function
This is the difference you will meet first. A function written inside another function cannot use the local variables or the parameters of that function. The Lua classic does not compile:
local function make_counter()
local n = 0
return function()
n = n + 1
return n
end
endsnippet:4: cannot capture local 'n' of an enclosing function
What a function can use:
- its own parameters and locals;
- global variables;
- the top-level locals of the file (declared outside any function): they act like variables of the module, shared by every function of the file;
- itself, when declared with
local function f(recursion works).
So the state goes in a top-level local, in a table passed as a parameter, or in a table used as an object:
local function new_counter()
return {n = 0}
end
local function bump(counter)
counter.n = counter.n + 1
return counter.n
end
local c = new_counter()
bump(c)
print(bump(c))2
A top-level for loop variable is a single variable of the file, not a fresh copy per turn: a function created in the loop sees its current value when called, not the value of its turn.
local handlers = {}
for i = 1, 3 do
handlers[i] = function() return i end
end
print(handlers[1](), handlers[3]())3 3
Store the value in a table (handlers[i] = {step = i}) when each one needs its own.
A function returns one value
return a, b is a compile error: a function returns a single value (return a table instead). Return a table:
local function stack_split(count)
return {stacks = count // 64, rest = count % 64}
end
local s = stack_split(1234)
print(s.stacks, s.rest)19 18
local a, b = f() compiles, but b is always nil. Multiple assignment itself works: a, b = b, a swaps two values.
The library follows the same rule. Where Lua returns several values, Brass returns one, or a table:
| Lua | Brass |
|---|---|
local i, j = s:find("x") | local i = s:find("x"), the end is i + #"x" - 1 |
local ok, err = pcall(f) | local r = pcall(f), then r.ok, r.value, r.error |
local x, y = term.getCursorPos() | local c = term.get_cursor(), then c.x, c.y |
local w, h = term.getSize() | local s = term.get_size(), then s.w, s.h |
local event, a, b = os.pullEvent() | local e = os.pull_event(), then e.name and named fields |
No ..., select or unpack
function f(...) is a compile error (variable arguments ('...') are not supported), and select, unpack and table.unpack do not exist. Give a function a table instead of a variable count of arguments: sum({3, 4, 5}). Library functions still accept several arguments where it makes sense: print, write, math.min, math.max, string.format, string.char.
The arguments of a program typed at the prompt (run stock north 64, or stock north 64) are in the global table arg: arg[1] is "north", arg[2] is "64" (text), and arg[0] is the path of the program.
No metatables
setmetatable, getmetatable, rawget, rawset, rawequal, rawlen and every __index, __call, __add... are absent. An object is a plain table that holds its data and its functions; obj:method() passes the table as self. To give several objects the same functions, copy them in, with table.copy for example:
local Machine = {}
function Machine.describe(self)
return self.name .. " at " .. self.speed .. " RPM"
end
local function new_machine(name, speed)
local m = table.copy(Machine)
m.name = name
m.speed = speed
return m
end
local press = new_machine("Mechanical Press", 64)
print(press:describe())Mechanical Press at 64 RPM
Programs in several files shows the same idea as a module that builds objects.
No coroutines
There is no coroutine library, and no ComputerCraft parallel. A Brass program runs one line of execution. To do several things at once (blink a lamp, watch a lever, answer the keyboard), write one event loop that handles every kind of event, with timers for the periodic jobs:
local blink = os.start_timer(0.5)
local lit = false
while true do
local e = os.pull_event()
if e.name == "timer" and e.id == blink then
lit = not lit
rs.set("top", lit)
blink = os.start_timer(0.5)
elseif e.name == "redstone" then
print("lever: " .. rs.get("left"))
elseif e.name == "key" and e.key == "enter" then
print("enter pressed")
end
endEvents builds this pattern step by step.
No goto
goto and labels (::continue::) do not exist: goto continue stops at syntax error near 'continue'. To skip the rest of a turn, put it in an if:
for _, item in ipairs({"iron", "", "gold"}) do
if item ~= "" then
print(item)
end
endiron gold
Strings
- No patterns.
string.findsearches the text exactly as written (its fourth argument is ignored) and returns only the start position.string.match,string.gmatchandstring.gsubdo not exist.Working with textshows how to do each usual job without them. string.formatknows%d %i %f %s %x %X %%, with the flags- 0 + space, a width and a precision of at most two digits.%q,%e,%g,%c,%oand%aare refused.- Characters, not bytes.
#"café"is 4 (5 in Lua),string.bytegives character codes up to 65535 ("€"is 8364),string.characcepts them, andupper/lowerhandle accents ("été"gives"ÉTÉ"). - No automatic conversion in arithmetic:
"10" + 1is an error, not 11.tonumberconverts...still turns numbers into text. - Escapes:
\n \t \r \\ \" \' \0 \xNNonly (no\ddd,\u{...},\z,\a...). Long strings have a single level,[[ ... ]](no[==[). - Extensions:
string.split,string.trim,string.starts,string.ends. - A string holds 65536 characters at most.
Numbers
- One kind of number, a 64-bit float. There is no integer subtype:
10 / 2prints5(Lua 5.3 prints5.0), andmath.type,math.tointeger,math.maxintegerandmath.modfdo not exist. Whole numbers are exact up to 2^53. - Numbers print like Lua's
%.14g:0.1 + 0.2shows0.3. - No bitwise operators:
&,|,~,<<,>>are compile errors (unexpected symbol '&'), and there is nobit32. Use arithmetic: bitnofxisx // 2 ^ n % 2. - Extension:
math.round.
Tables and loops
for v in list dogoes through the values of a list (a Brass extension).for k, v in twithoutpairsis an error that tells you so.pairsgoes through the list part in order, then the other keys in the order they were added: the same order at every run, unlike Lua. Adding or removing keys during the loop is safe.table.sortis stable (equal elements keep their order).table.remove(t)on an empty table returnsnil(table.remove(t, 1)is an error).- Extensions:
table.copy(a shallow copy),table.contains,table.keys. - Missing:
table.unpack,table.pack,table.move, andnext. To test whether a table is empty, use#table.keys(t) == 0. !=works like~=.
Errors
pcallreturns a table:{ok = true, value = ...}or{ok = false, error = "..."}. Beware of the Lua habit, which compiles but does not do what you think:
local ok, err = pcall(error, "boom")
print(type(ok), err)
local r = pcall(error, "boom")
print(r.ok, r.error)table nil false snippet:3: boom
error(message)always adds the positionfile:line:of the call; its second argument (the level) is ignored. A value that is not a string becomes text, so you cannot throw a table to carry data.- No
xpcalland no traceback: a message names the line where the error happened, not the calls that led there. - Ctrl+T cannot be caught: there is no
terminateevent and noos.pullEventRaw.
Errors and debugging covers all of this in depth.
Missing globals
load, loadstring, dofile, loadfile, next, select, unpack, rawget and friends, setmetatable, getmetatable, collectgarbage, xpcall, _G, _ENV and _VERSION do not exist, nor do the libraries coroutine, io, debug, utf8, package and bit32. In os, only the functions of os exist (no os.date, os.exit, os.getenv, os.remove...). Calling a missing function stops with attempt to call a nil value (global 'load'). To run another file, use import().
The APIs, compared with ComputerCraft
Names in snake_case
Every Brass name is in lower case with underscores: os.pull_event, os.start_timer, os.queue_event, os.day_time, term.set_cursor, term.get_size, fs.make_dir, fs.is_dir. A ComputerCraft name stops with attempt to call a nil value (field 'pullEvent').
Events are tables
os.pull_event([name]) returns one table, with the name in e.name and the data in named fields:
-- ComputerCraft: local event, key = os.pullEvent("key")
local e = os.pull_event("key")
if e.key == "enter" then
print("confirmed")
end| event | fields |
|---|---|
timer | id |
key | key: a name ("enter", "backspace", "delete", "up", "down", "left", "right", "home", "end", "tab") |
char | char: the character typed (letters, digits, space arrive here, not as key) |
paste | text |
click, drag | x, y (character), px, py (pixel), button (1, 2, 3), source ("terminal" or "monitor") |
redstone | none: read the sides with rs.get |
message | sender, channel, data, via, distance |
disk | inserted (true or false) |
Keys are names, not numbers: there is no keys table, and no "held" flag. os.queue_event(name, data) carries a single value, in e.data. As in ComputerCraft, waiting for one name throws away the other events, and the queue holds 256 events at most. The full list is in Events.
Redstone: rs
There is no redstone table, only rs, and it speaks in levels:
| ComputerCraft | Brass |
|---|---|
rs.setOutput("top", true) | rs.set("top", true) or rs.set("top", 15) |
rs.setAnalogOutput("top", 7) | rs.set("top", 7) |
rs.getInput("left") | rs.get("left") > 0 |
rs.getAnalogInput("left") | rs.get("left") |
rs.getSides() | rs.sides() |
rs.get returns a number, and 0 is true in a condition: if rs.get("left") then is always taken. Compare with > 0. Sides are front, back, left, right, top, bottom, left and right as seen when facing the screen. See rs.
Peripherals
peripheral.wrap(name) returns a table of functions (or nil when there is no device). Call them with a dot, as with ComputerCraft's wrapped peripherals:
local light = peripheral.wrap("left")
light.set("green")A colon (light:set("green")) would hand the table itself to the function as its first argument, and the call fails with bad argument #1 to 'set' (string expected, got table). (The colon is for your own objects, see above.)
| ComputerCraft | Brass |
|---|---|
peripheral.getNames() | peripheral.list(), or peripheral.list("inventory") for one type |
peripheral.getType(name) | peripheral.type(name) |
peripheral.find(type): every match | peripheral.find(type): the first match only; wrap the names of peripheral.list(type) for all of them |
peripheral.call(name, method, ...) | the same |
peripheral.getMethods(name) | the wrapped table is a plain table: table.keys(device) |
network names like "monitor_0" | faces ("left") or "type@x,y,z" for a block on a Data Cable |
The devices and their methods are Brass's own (Create machines, Item Vaults, Traffic Lights, sensors): see Every device. You do not write to a monitor through peripheral: a monitor touching the computer shows its screen, so print, term and gfx draw on it directly (wrapping it only tells its size, @monitor.size).
Terminal and colours
term.setTextColorandterm.setBackgroundColorareterm.set_fgandterm.set_bg;term.setCursorPosisterm.set_cursor;term.get_cursorandterm.get_size(ComputerCraft'sgetCursorPosandgetSize) return tables,{x = ..., y = ...}and{w = ..., h = ...}.- Colours are numbers from 0 to 15, not ComputerCraft's powers of two, and they live in
term.colors(there is no globalcolorsorcolours):
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| white | orange | magenta | light_blue | yellow | lime | pink | gray | light_gray | cyan | purple | blue | brown | green | red | black |
A ComputerCraft value such as colors.red (16384) stops with bad argument #1 to 'set_fg' (color must be 0..15). The Brass number is the power of two of the ComputerCraft one: 16384 is 2^14, so red is 14. The gfx functions also accept the names as text: gfx.rect(1, 1, 20, 10, "red").
- There is no
term.blit,term.redirect,term.isColor,windoworpaintutils. Drawing is done withgfx(pixels, lines, shapes, small text). The screens of the first ages (paper, green and amber phosphor) draw everything in their single ink.
Files
There are no file handles and no io. A file is read and written in one call:
| ComputerCraft | Brass |
|---|---|
fs.open(p, "r") then h.readAll(), h.close() | fs.read(p): the whole text, or nil |
reading line by line with h.readLine() | fs.read(p):split("\n") |
fs.open(p, "w") then h.write(t), h.close() | fs.write(p, t) (creates the folders on the way) |
fs.open(p, "a") | fs.append(p, t) |
fs.makeDir, fs.isDir, fs.getSize | fs.make_dir, fs.is_dir, fs.size |
fs.getFreeSpace, fs.getCapacity | fs.free(), fs.capacity() |
fs.combine(a, b) | a .. "/" .. b |
Paths are relative to the current folder (fs.cwd()), or to the root with a leading /. Names use letters, digits, _, . and -. See fs.
Network
net replaces rednet, with no rednet.open: the Data Cable, Radio Modem or Wi-Fi Router does the connecting.
| ComputerCraft | Brass |
|---|---|
rednet.open("back") | nothing to do |
rednet.send(id, msg, protocol) | net.send(id, data, channel): true if delivered |
rednet.broadcast(msg, protocol) | net.broadcast(data, channel): the number of computers reached |
local id, msg = rednet.receive(protocol, 5) | local m = net.receive(5): m.sender, m.data, m.channel, or nil after 5 s |
os.getComputerID() | os.id() or net.id() |
net.receive takes no channel: test m.channel yourself. Messages are copies of plain data (numbers, text, booleans, tables of those), up to 32768 characters. net exists from the Minicomputer on. See net and Networks.
Programs and the shell
- The interactive interpreter is
brass, notlua. - The boot file is
startup, exactly (nostartup.lua, nostartupfolder). - Program arguments come in the global
arg, not in.... - There is no
shellAPI (noshell.run) and nomultishell: one program runs at a time. A program starts another file withimport, which also loads libraries with paths relative to the file (import "lib/text"), seePrograms in several files.requireis the same function. - No
textutils(netsends tables as they are; to save a table in a file, write its fields as text), nosettings(use a settings file, seeWorking with text). os.getComputerLabelandos.setComputerLabelareos.label()andos.label(text).readtakes one optional argument, a mask character for passwords:read("*"). No history or completion arguments.- No
http,disk,gps,turtleorpocket. In their place, libraries for Create:link(Redstone Links),display(Display Links),vehicle(Create Aeronautics).
Time
sleep(seconds)waitsceil(seconds × 20)ticks, at least one:sleep(0)waits one tick. There is noos.sleep.- **
os.time()is not the time of day**: it counts the ticks since the computer started. The time of day isos.day_time(), from 0 to 23999 (6000 is noon).os.clock()gives the seconds since the start. There is noos.epoch,os.dayoros.date. os.start_timer(seconds)returns an id and later queues{name = "timer", id = ...}. There is noos.cancelTimer: keep the id of the timer you care about and ignore the others. Noos.setAlarmeither: compareos.day_time()in a loop. 256 timers can wait at the same time.
Time and timers has the details.
Speed: an instruction budget, never "too long without yielding"
ComputerCraft kills a program that computes for a few seconds without yielding (Too long without yielding), so long loops need sleep(0) or os.queueEvent tricks. Brass never does. Each computer runs a fixed number of instructions per tick; when they are spent, the program pauses where it is and goes on at the next tick. A while true do end is harmless to the server: it only uses its own computer's time.
| computer | instructions per second at 256 RPM |
|---|---|
| Tube Computer | 400 |
| Transistor Mainframe | 1,600 |
| Minicomputer | 6,000 |
| Personal Computer | 24,000 |
| Microcontroller | 8,000 |
| Modern Computer | 100,000 |
The budget follows the rotation of the shaft that drives the computer: half the speed at 128 RPM, nothing without rotation. Big jobs of the library cost extra instructions (sorting, long texts, walking a network). Two things follow: a loop that checks something over and over (busy waiting) wastes the time the rest of your program needs, so wait with os.pull_event or sleep; and on the oldest computers, fewer instructions mean simpler programs. See Speed, memory and limits.
Memory in cells
A ComputerCraft program can use as much memory as the server's Java gives it. A Brass computer has a fixed memory, counted in cells:
| computer | memory |
|---|---|
| Tube Computer | 2,048 cells |
| Transistor Mainframe | 8,192 cells |
| Minicomputer | 32,768 cells |
| Personal Computer | 131,072 cells |
| Microcontroller | 16,384 cells |
| Modern Computer | 1,048,576 cells |
A table costs 4 cells plus 2 per entry, a string 1 cell plus 1 per 8 characters, a function call 8 cells while it runs. Going over the capacity stops the program with out of memory. os.memory() returns {used = ..., total = ...}, and the mem command shows it. Other hard limits: 200 nested calls (stack overflow), 65536 characters per string, 256 queued events, 256 waiting timers. See Limits.
Side by side
| In ComputerCraft | In Brass |
|---|---|
local ok, err = pcall(f, x) | local r = pcall(f, x), then r.ok, r.value, r.error |
return a, b | return {a = a, b = b} |
function f(...) | function f(list) |
local args = {...} (program arguments) | arg[1], arg[2]... |
| closures over a function's locals | top-level locals, parameters, tables with self |
setmetatable(obj, {__index = Class}) | copy the functions in: table.copy(Class) |
coroutine, parallel.waitForAny | one event loop with os.pull_event and timers |
s:gsub("a", "b") | table.concat(s:split("a"), "b") |
s:match("^%s*(.-)%s*$") | s:trim() |
bit32.band(a, b), a & b | arithmetic with // and % |
os.pullEvent("key") | os.pull_event("key"), returns a table |
keys.enter | "enter" (in e.key) |
os.startTimer(1) | os.start_timer(1) |
os.cancelTimer(id) | ignore the timer's event |
os.time() (time of day) | os.day_time() (0 to 23999) |
os.epoch("utc") | no real time: os.time() counts ticks since start |
os.getComputerID() | os.id() |
os.setComputerLabel("x") | os.label("x") |
sleep(0.05) / os.sleep | sleep(0.05) (one tick) |
redstone.setOutput("top", true) | rs.set("top", true) |
redstone.getAnalogInput("left") | rs.get("left") |
peripheral.find("monitor") | a monitor shows the computer's screen: just print |
peripheral.getNames() | peripheral.list() |
term.setTextColor(colors.red) | term.set_fg(term.colors.red) (14) |
colors.white = 1, colors.black = 32768 | term.colors.white = 0, term.colors.black = 15 |
term.setCursorPos(x, y) | term.set_cursor(x, y) |
local x, y = term.getCursorPos() | local c = term.get_cursor() |
local w, h = term.getSize() | local s = term.get_size() |
paintutils.drawFilledBox(...) | gfx.rect(x, y, w, h, color, true) |
fs.open(p, "r").readAll() | fs.read(p) |
fs.open(p, "w") + write + close | fs.write(p, text) |
fs.makeDir(p) | fs.make_dir(p) |
rednet.open("back") | nothing |
rednet.send(id, msg, "proto") | net.send(id, msg, "proto") |
rednet.receive(nil, 5) | net.receive(5), returns a table or nil |
textutils.serialize(t) | no equivalent: net sends tables as they are |
shell.run("prog") | import "prog" |
lua (interactive prompt) | brass |
startup.lua | startup |
| "Too long without yielding" | never: the budget pauses and resumes the program |
| unlimited memory | 2 K to 1 M cells, out of memory |
See also
Values and variables,Functions,Tables: the language from the start.Errors and debugging: the error messages, andpcallin practice.Cheat sheet: the whole language and its libraries on one page.