Create: Computing AgesBrass Docs
The Brass language

Tables

The one data structure of Brass: lists, dictionaries, records, sets and objects, how to go through them, and what they cost.

A table is a container: it holds values, each one stored under a key. It is the only data structure of Brass, and it does every job. With keys 1, 2, 3... it is a list (the trains of a line, the steps of a sequence). With names as keys it is a dictionary or a record (the stock of each item, the settings of a machine). The libraries use tables everywhere too: chest.list() gives a table of slots, os.pull_event an event table, term.get_size a table {w = ..., h = ...}.

Brass
local line = {"press", "mixer", "saw"}             -- a list
local press = {kind = "press", rpm = 64}           -- a record
print(line[1], #line, press.rpm)
Screen
press   3   64

Curly braces {} build a table. This page assumes you know values, loops and functions; the table reference details every function of the table library.

Lists

A list is a table whose keys are 1, 2, 3... in order. Write the values between braces, separated by commas; read one with its position in square brackets. Positions start at 1, not 0. Reading a position that holds nothing gives nil, without an error. #list is the length of the list.

The table library does the usual work on lists:

calldoes
table.insert(list, value)adds value at the end (same as list[#list + 1] = value)
table.insert(list, pos, value)inserts at position pos, shifting the next ones up
table.remove(list)removes the last element and returns it (nil on an empty list)
table.remove(list, pos)removes the element at pos, shifting the next ones down, and returns it
table.concat(list, sep)joins strings and numbers into one string
table.sort(list)sorts in place, smallest first (see functions as values for other orders)
table.contains(list, value)true if the value is in the table
Brass
local queue = {"iron", "gold"}
table.insert(queue, "copper")      -- at the end
table.insert(queue, 1, "coal")     -- at the front
print(#queue, table.concat(queue, ", "))
local first = table.remove(queue, 1)
print(first, table.concat(queue, ", "))
print(queue[1], queue[10])
Screen
4   coal, iron, gold, copper
coal    iron, gold, copper
iron    nil

A position outside the list, like 0 or 11 here, is not an error when reading; table.insert and table.remove refuse one that is out of the list with bad argument #2 to 'insert' (position out of bounds) (or 'remove'). So table.remove(list, 1) on an empty list is an error, while table.remove(list) just gives nil.

A list that keeps only its last entries is a small log: add at the end, and remove the oldest when it gets too long. A data logger keeps the last hour of a sensor this way:

Brass
local log = {}
local function record(value)
  table.insert(log, value)
  if #log > 5 then
    table.remove(log, 1)   -- forget the oldest
  end
end
for reading = 10, 80, 10 do
  record(reading)
end
print(table.concat(log, " "))
Screen
40 50 60 70 80

Holes and the length

# is reliable only for a list without holes (a nil between two values). Brass keeps the list part of a table in one piece: # counts up to its last element, and only shrinks when that last element is removed. Setting an element in the middle to nil leaves a hole that # still counts, while ipairs stops at it:

Brass
local line = {"press", "mixer", "saw", "drill"}
line[2] = nil              -- a hole
print(#line)
for i, machine in ipairs(line) do
  print(i, machine)
end
local odd = {"a", nil, "c"}
print(#odd)
Screen
4
1   press
1

In {"a", nil, "c"} the "c" does not even join the list part: #odd is 1. (Lua programmers know the result of # as a border, any one of which Lua may return when there are holes. In Brass it is always the size of the list part, so the results above are the same on every run.) The rule is simple: never leave holes in a list. To delete an element, use table.remove, which closes the gap. When a table has gaps by nature (the slots of a chest, where empty slots are missing), do not treat it as a list: go through it with pairs (see going through a table).

Removing while going through

Removing elements from a list while walking it forwards skips some: after a removal, the next element moves back into the position you just handled. Walk the list backwards instead, from #list down to 1:

Brass
local loot = {"cobblestone", "cobblestone", "diamond", "cobblestone"}
for i = #loot, 1, -1 do
  if loot[i] == "cobblestone" then
    table.remove(loot, i)
  end
end
print(table.concat(loot, ", "))
Screen
diamond

Dictionaries: any key

A key does not have to be a number. Any value except nil can be a key: a string most of the time, but also a number, a boolean, even another table. A table used this way is a dictionary: you look a value up by its key.

Brass
local stock = {}
stock["minecraft:iron_ingot"] = 128
stock["create:brass_ingot"] = 40
stock.coal = 12                       -- same as stock["coal"]
local item = "create:brass_ingot"
print(stock[item], stock.coal, stock.diamond)
stock.coal = nil                      -- removes the key
for name, count in pairs(stock) do
  print(name, count)
end
Screen
40  12  nil
minecraft:iron_ingot    128
create:brass_ingot  40
  • t.name is a shortcut for t["name"], for keys that are valid names. Use brackets when the key is in a variable (stock[item]), contains other characters (stock["minecraft:iron_ingot"]), or is a number.
  • Beware: t.item is the key "item", while t[item] uses the value of the variable item.
  • Reading a missing key gives nil. Giving a key the value nil removes it from the table.
  • t[nil] = 1 is an error: table index is nil.
  • 1 and "1" are two different keys: a number read from text must go through tonumber before it finds the number key. 1 and 1.0 are the same key.

In a table constructor, name = value sets a key that is a valid name, and [expression] = value sets any other key:

Brass
local recipes = {
  ["minecraft:iron_ingot"] = "smelt raw iron",
  ["create:brass_ingot"] = "mix copper and zinc",
  [64] = "a full stack",
}
print(recipes["create:brass_ingot"])
print(recipes[64])
Screen
mix copper and zinc
a full stack

# counts only the list part: the length of a dictionary is 0. To count its entries, go through it with pairs.

Nested tables

A value in a table can be another table, as deep as you need. A whole factory fits in one table:

Brass
local factory = {
  name = "Brass Works",
  machines = {
    {kind = "press", rpm = 64, running = true},
    {kind = "mixer", rpm = 128, running = false},
  },
  storage = {iron = 512, zinc = 96},
}
print(factory.machines[2].kind, factory.storage.zinc)
factory.machines[2].running = true
print(#factory.machines, factory.machines[2].running)
Screen
mixer   96
2   true

Reading through a missing level fails: factory.depot.size gives attempt to index a nil value (field 'depot'), because factory.depot is nil. Guard it with factory.depot and factory.depot.size, or check the level first.

References, not copies

A variable does not contain a table: it refers to it. Assigning a table to another variable, or passing it to a function, does not copy it: both names reach the same table, and a change through one is seen through the other. That is how a function can fill a table it receives.

Brass
local vault = {iron = 64}
local same = vault            -- the same table, two names
same.iron = 0
print(vault.iron)
local copy = table.copy(vault)
copy.iron = 99
print(vault.iron, copy.iron)
print(vault == same, vault == copy)
print({} == {})
Screen
0
0   99
true    false
false
  • == on tables compares identity: two tables are equal only if they are the same table, even when their contents match. To compare contents, compare the fields.
  • table.copy makes a shallow copy: a new table with the same keys and values. A table inside is not copied; the copy refers to the same inner table. For a full copy, copy the inner tables too:
Brass
local function deep_copy(t)
  local result = {}
  for k, v in pairs(t) do
    if type(v) == "table" then
      v = deep_copy(v)
    end
    result[k] = v
  end
  return result
end

local plan = {name = "line A", speeds = {64, 128}}
local backup = deep_copy(plan)
plan.speeds[1] = 0
print(backup.speeds[1])
Screen
64

(A table that contains itself would make deep_copy recurse forever, until stack overflow.)

Going through a table

Three loops, already met in Conditions and loops:

loopvisitsuse it for
for i, v in ipairs(t) dot[1], t[2]... until the first nillists
for v in t dothe same values, without the positionlists, when the position does not matter
for k, v in pairs(t) doevery keyeverything else

The order of pairs is predictable in Brass (in Lua it is not): first the list part, positions 1, 2, 3... in order, then all the other keys in the order they were first added. A key removed and added again goes to the end. Changing or removing existing keys during a pairs loop is safe; keys added during the loop may not be visited.

chest.list() (inventory.list()) is the typical table with gaps: it has a key for each slot that holds something, and none for empty slots. ipairs would stop at the first empty slot; pairs sees them all:

Brass
-- what chest.list() returns: only the slots that hold something
local slots = {}
slots[1] = {name = "minecraft:iron_ingot", count = 64}
slots[2] = {name = "minecraft:iron_ingot", count = 12}
slots[5] = {name = "create:andesite_alloy", count = 30}

print("#slots = " .. #slots)
for slot, item in pairs(slots) do
  print(slot, item.count, item.name)
end
Screen
#slots = 2
1   64  minecraft:iron_ingot
2   12  minecraft:iron_ingot
5   30  create:andesite_alloy

Each item also has a display field, its name as shown in game. table.keys(t) returns a list of the keys of a table, handy to sort them before showing them.

Records: a list of machines

A record is a table with named fields that describes one thing. A list of records describes many things of the same kind, and loops over it read like sentences:

Brass
local machines = {
  {name = "Crusher A", rpm = 128, overstressed = false},
  {name = "Crusher B", rpm = 0, overstressed = true},
  {name = "Press", rpm = 64, overstressed = false},
}
for _, m in ipairs(machines) do
  local state = "running"
  if m.overstressed then
    state = "OVERSTRESSED"
  elseif m.rpm == 0 then
    state = "stopped"
  end
  print(string.format("%-10s %4d RPM  %s", m.name, m.rpm, state))
end
Screen
Crusher A   128 RPM  running
Crusher B     0 RPM  OVERSTRESSED
Press        64 RPM  running

With real machines, each record would be filled from the blocks themselves (@kinetic.speed, kinetic.overstressed()) and the list shown on a monitor: see the factory dashboard.

Objects: records with methods

Put functions in a record and it becomes an object: data plus the methods that work on it, called with :. To make many objects of the same kind, write the methods once in a table, and a function that builds each object and copies the methods into it. (Brass has no metatables, so each object carries its own references to the methods; the functions themselves are shared, not duplicated.)

Brass
local Tank = {}

function Tank:fill(mb)
  self.amount = math.min(self.capacity, self.amount + mb)
end

function Tank:percent()
  return math.floor(self.amount * 100 / self.capacity)
end

local function new_tank(fluid, capacity)
  local tank = {fluid = fluid, amount = 0, capacity = capacity}
  for name, method in pairs(Tank) do
    tank[name] = method
  end
  return tank
end

local lava = new_tank("lava", 8000)
local water = new_tank("water", 4000)
lava:fill(6000)
lava:fill(1000)
water:fill(5000)
print(lava.fluid .. ": " .. lava:percent() .. " %")
print(water.fluid .. ": " .. water:percent() .. " %")
Screen
lava: 87 %
water: 100 %

Sets

A set answers one question: is this value in it? Use the values as keys and true as the value. Checking is a single lookup, allowed[name], however big the set is.

Brass
local allowed = {Steve = true, Alex = true}
allowed["Notch"] = true      -- add
allowed.Alex = nil           -- remove
for _, player in ipairs({"Steve", "Alex", "Herobrine", "Notch"}) do
  if allowed[player] then
    print(player .. ": welcome")
  else
    print(player .. ": access denied")
  end
end
Screen
Steve: welcome
Alex: access denied
Herobrine: access denied
Notch: welcome

table.contains(list, value) gives the same answer on a list, but it looks through every element: fine for ten names, slower for a thousand. A set is the tool for a door that lets some players in.

Counting and grouping

To count how many times each value appears, use the value as a key and add one each time. The (counts[k] or 0) gives 0 the first time a key is seen:

Brass
local passed = {"Red Line", "Blue Line", "Red Line", "Freight", "Red Line", "Blue Line"}
local counts = {}
for _, train in ipairs(passed) do
  counts[train] = (counts[train] or 0) + 1
end
for train, n in pairs(counts) do
  print(train, n)
end
Screen
Red Line    3
Blue Line   2
Freight 1

Grouping goes one step further: each key holds a table that collects everything belonging to it. Here, the contents of a chest grouped by mod, using the part of the item id before the ::

Brass
-- what chest.list() could return
local items = {
  {name = "minecraft:iron_ingot", count = 64},
  {name = "create:andesite_alloy", count = 30},
  {name = "minecraft:coal", count = 12},
  {name = "create:brass_ingot", count = 9},
  {name = "computingages:copper_wire", count = 4},
}

local by_mod = {}
for _, item in pairs(items) do
  local parts = item.name:split(":")      -- {"minecraft", "iron_ingot"}
  local mod = parts[1]
  if by_mod[mod] == nil then
    by_mod[mod] = {total = 0, kinds = {}}
  end
  local group = by_mod[mod]
  group.total = group.total + item.count
  table.insert(group.kinds, parts[2])
end

for mod, group in pairs(by_mod) do
  print(mod .. ": " .. group.total .. " (" .. table.concat(group.kinds, ", ") .. ")")
end
Screen
minecraft: 76 (iron_ingot, coal)
create: 39 (andesite_alloy, brass_ingot)
computingages: 4 (copper_wire)

The loop uses pairs rather than ipairs because the real chest.list() skips empty slots. string.split cuts the id in two (see Strings).

What tables cost

Each computer has a memory measured in cells, from 2,048 for a Tube Computer to 1,048,576 for a Modern Computer (131,072 for a Personal Computer). Tables are where most of it goes:

thingcells
a table4
each entry of a table (key and value)2
a string1, plus 1 per 8 characters

A list of 1,000 sensor readings costs about 2,004 cells: nothing for a Personal Computer, the whole memory of a Tube Computer. Memory is given back on its own when nothing refers to a table any more (the local of a function that has returned, a field or a variable set to nil). A program that keeps more than the computer can hold stops with out of memory.

So keep only what you need: a rolling log rather than an endless one, and counts rather than the full list of events. os.memory tells how much is in use; Performance explains the budget of instructions and cells in detail.