Create: Computing AgesBrass Docs
The Brass language

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, numeric for, for k, v in pairs(t), break, do ... end, return.
  • Comments -- and --[[ ... ]], strings in "...", '...' and [[ ... ]].
  • Tables indexed from 1, #t, t.name and t[key], {1, 2, x = 3}.
  • and, or, not, with the same "only nil and false are false" rule (so 0 and "" are true).
  • // (floor division) and % as in Lua 5.3, ^ for powers.
  • Method calls obj:method(x) and function obj:method(x) with self, call sugar f "text" and f {table}.
  • tostring, tonumber, type, pairs, ipairs, error, assert, and the usual functions of string, table and math.

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:

Brass
local function make_counter()
  local n = 0
  return function()
    n = n + 1
    return n
  end
end
Screen
snippet: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:

Brass
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))
Screen
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.

Brass
local handlers = {}
for i = 1, 3 do
  handlers[i] = function() return i end
end
print(handlers[1](), handlers[3]())
Screen
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:

Brass
local function stack_split(count)
  return {stacks = count // 64, rest = count % 64}
end
local s = stack_split(1234)
print(s.stacks, s.rest)
Screen
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:

LuaBrass
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:

Brass
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())
Screen
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:

Brass
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
end

Events 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:

Brass
for _, item in ipairs({"iron", "", "gold"}) do
  if item ~= "" then
    print(item)
  end
end
Screen
iron
gold

Strings

  • No patterns. string.find searches the text exactly as written (its fourth argument is ignored) and returns only the start position. string.match, string.gmatch and string.gsub do not exist. Working with text shows how to do each usual job without them.
  • string.format knows %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, %o and %a are refused.
  • Characters, not bytes. #"café" is 4 (5 in Lua), string.byte gives character codes up to 65535 ("€" is 8364), string.char accepts them, and upper/lower handle accents ("été" gives "ÉTÉ").
  • No automatic conversion in arithmetic: "10" + 1 is an error, not 11. tonumber converts. .. still turns numbers into text.
  • Escapes: \n \t \r \\ \" \' \0 \xNN only (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 / 2 prints 5 (Lua 5.3 prints 5.0), and math.type, math.tointeger, math.maxinteger and math.modf do not exist. Whole numbers are exact up to 2^53.
  • Numbers print like Lua's %.14g: 0.1 + 0.2 shows 0.3.
  • No bitwise operators: &, |, ~, <<, >> are compile errors (unexpected symbol '&'), and there is no bit32. Use arithmetic: bit n of x is x // 2 ^ n % 2.
  • Extension: math.round.

Tables and loops

  • for v in list do goes through the values of a list (a Brass extension). for k, v in t without pairs is an error that tells you so.
  • pairs goes 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.sort is stable (equal elements keep their order). table.remove(t) on an empty table returns nil (table.remove(t, 1) is an error).
  • Extensions: table.copy (a shallow copy), table.contains, table.keys.
  • Missing: table.unpack, table.pack, table.move, and next. To test whether a table is empty, use #table.keys(t) == 0.
  • != works like ~=.

Errors

  • pcall returns a table: {ok = true, value = ...} or {ok = false, error = "..."}. Beware of the Lua habit, which compiles but does not do what you think:
Brass
local ok, err = pcall(error, "boom")
print(type(ok), err)
local r = pcall(error, "boom")
print(r.ok, r.error)
Screen
table   nil
false   snippet:3: boom
  • error(message) always adds the position file: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 xpcall and 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 terminate event and no os.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:

Brass
-- ComputerCraft: local event, key = os.pullEvent("key")
local e = os.pull_event("key")
if e.key == "enter" then
  print("confirmed")
end
eventfields
timerid
keykey: a name ("enter", "backspace", "delete", "up", "down", "left", "right", "home", "end", "tab")
charchar: the character typed (letters, digits, space arrive here, not as key)
pastetext
click, dragx, y (character), px, py (pixel), button (1, 2, 3), source ("terminal" or "monitor")
redstonenone: read the sides with rs.get
messagesender, channel, data, via, distance
diskinserted (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:

ComputerCraftBrass
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:

Brass
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.)

ComputerCraftBrass
peripheral.getNames()peripheral.list(), or peripheral.list("inventory") for one type
peripheral.getType(name)peripheral.type(name)
peripheral.find(type): every matchperipheral.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.setTextColor and term.setBackgroundColor are term.set_fg and term.set_bg; term.setCursorPos is term.set_cursor; term.get_cursor and term.get_size (ComputerCraft's getCursorPos and getSize) 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 global colors or colours):
0123456789101112131415
whiteorangemagentalight_blueyellowlimepinkgraylight_graycyanpurplebluebrowngreenredblack

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, window or paintutils. Drawing is done with gfx (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:

ComputerCraftBrass
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.getSizefs.make_dir, fs.is_dir, fs.size
fs.getFreeSpace, fs.getCapacityfs.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.

ComputerCraftBrass
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, not lua.
  • The boot file is startup, exactly (no startup.lua, no startup folder).
  • Program arguments come in the global arg, not in ....
  • There is no shell API (no shell.run) and no multishell: one program runs at a time. A program starts another file with import, which also loads libraries with paths relative to the file (import "lib/text"), see Programs in several files. require is the same function.
  • No textutils (net sends tables as they are; to save a table in a file, write its fields as text), no settings (use a settings file, see Working with text).
  • os.getComputerLabel and os.setComputerLabel are os.label() and os.label(text).
  • read takes one optional argument, a mask character for passwords: read("*"). No history or completion arguments.
  • No http, disk, gps, turtle or pocket. In their place, libraries for Create: link (Redstone Links), display (Display Links), vehicle (Create Aeronautics).

Time

  • sleep(seconds) waits ceil(seconds × 20) ticks, at least one: sleep(0) waits one tick. There is no os.sleep.
  • **os.time() is not the time of day**: it counts the ticks since the computer started. The time of day is os.day_time(), from 0 to 23999 (6000 is noon). os.clock() gives the seconds since the start. There is no os.epoch, os.day or os.date.
  • os.start_timer(seconds) returns an id and later queues {name = "timer", id = ...}. There is no os.cancelTimer: keep the id of the timer you care about and ignore the others. No os.setAlarm either: compare os.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.

computerinstructions per second at 256 RPM
Tube Computer400
Transistor Mainframe1,600
Minicomputer6,000
Personal Computer24,000
Microcontroller8,000
Modern Computer100,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:

computermemory
Tube Computer2,048 cells
Transistor Mainframe8,192 cells
Minicomputer32,768 cells
Personal Computer131,072 cells
Microcontroller16,384 cells
Modern Computer1,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 ComputerCraftIn Brass
local ok, err = pcall(f, x)local r = pcall(f, x), then r.ok, r.value, r.error
return a, breturn {a = a, b = b}
function f(...)function f(list)
local args = {...} (program arguments)arg[1], arg[2]...
closures over a function's localstop-level locals, parameters, tables with self
setmetatable(obj, {__index = Class})copy the functions in: table.copy(Class)
coroutine, parallel.waitForAnyone 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 & barithmetic 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.sleepsleep(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 = 32768term.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 + closefs.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.luastartup
"Too long without yielding"never: the budget pauses and resumes the program
unlimited memory2 K to 1 M cells, out of memory

See also