Create: Computing AgesBrass Docs
The Brass language

Values and variables

Comments, numbers, text, true and false, nil, variables, and the operators that combine them.

A program works on values: the number of iron ingots in a vault, the name of a player, whether a lever is on. This page shows the kinds of values Brass knows, how to keep them in variables, and how to combine them with operators. Everything else builds on it: conditions and loops, functions and tables.

If you have never programmed, read the page in order and try the examples: write them in a file with edit test, run them with test (see the shell and the editor). If you know Lua already, skim it and read Brass for Lua programmers: the differences are few, but they matter.

Brass
-- How many stacks fill a vault?
local ingots = 1728          -- a number
local item = "iron ingot"    -- a string (some text)
local full = ingots >= 1728  -- a boolean (true or false)
print(ingots // 64 .. " stacks of " .. item)
print("full:", full)
Screen
27 stacks of iron ingot
full:   true
Try one line at a time

Type brass at the prompt to open the interactive interpreter: each line you type runs at once, and the value of an expression is printed. exit leaves it. Each line is its own little program, so a local variable is forgotten at the next line: use a global (x = 5) there.

Terminal
> brass
Brass 1.0 - type 'exit' to leave.
brass> 1728 // 64
27
brass> exit

Comments

A comment is a note for humans: the computer skips it. -- starts a comment that runs to the end of the line, and --[[ ... ]] is a block comment that can span several lines.

Brass
--[[
  Mechanical Press line
  lever on the left, alarm lamp on top
]]
local rpm = 64   -- speed of the press
-- print("debug: " .. rpm)
print(rpm)
Screen
64

Use comments to explain why the code does something ("the vault holds 1728 ingots"), not what it does: the code already says that. A comment is also a quick way to switch a line off while you test, like the print above. In the editor, Ctrl+/ (Ctrl+: on AZERTY) comments or uncomments the selected lines.

A block comment ends at the first ]], so it cannot contain ]] itself.

The six types

Every value has a type. Brass has six:

typeexamplesused for
nilnil"no value": a variable never set, a missing field, an empty slot
booleantrue, falseyes or no: is the lever on, is the tank full
number64, -3.5, 1e3, 0xFFcounts, speeds, coordinates, redstone strength
string"iron", 'top'text: item names, sides, messages
table{1, 2, 3}, {rpm = 64}lists and records, see Tables
functionprint, function(x) return x * 2 endcode you can call, see Functions

type tells you the type of a value, as a string:

Brass
print(type(64), type("top"), type(true))
print(type(nil), type({}), type(print))
Screen
number  string  boolean
nil table   function

Values have types, variables do not: the same variable can hold a number now and a string later. When a value comes from outside (a message from another computer, a line typed by the player), type(v) == "number" checks it is what you expect before you use it.

Numbers

Brass has a single kind of number: a 64-bit floating point number (a "double"). There is no separate integer type: 7 / 2 is 3.5, and a whole number is simply a number with nothing after the point. You can write numbers in several ways:

writtenvalue
64, -12whole numbers
3.5, .5decimals (the point is a dot, never a comma)
1e3, 2.5e-3with an exponent: 1e3 is 1000, 2.5e-3 is 0.0025
0xFFhexadecimal: 255
Brass
print(12, 3.5, .5, 1e3, 0xFF)
print(7 / 2, 10 / 2, 2 ^ 10)
Screen
12  3.5 0.5 1000    255
3.5 5   1024

How numbers print

  • A whole number prints **without .0**: 10 / 2 shows 5, not 5.0.
  • At most 14 significant digits are shown: 1 / 3 shows 0.33333333333333, math.pi shows 3.1415926535898.
  • Very large or very small numbers use an exponent: 1e15 shows 1e+15, 0.00001 shows 1e-05.
  • Dividing by zero is not an error. It gives inf (infinity), -inf, or nan ("not a number", for 0 / 0), so check a divisor yourself when it can be zero.
Brass
print(1 / 3, 1e15, 2 ^ 53)
print(1 / 0, -1 / 0, 0 / 0)
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)
Screen
0.33333333333333    1e+15   9.007199254741e+15
inf -inf    nan
0.3 false

Whole numbers are exact up to 2^53 (about 9 million billion): counting items, ticks or blocks never loses precision. Decimals are approximations, as on every computer: 0.1 + 0.2 is a hair above 0.3. It prints 0.3 because only 14 digits are shown, yet 0.1 + 0.2 == 0.3 is false. Compare decimals with a tolerance instead: math.abs(a - b) < 0.0001.

To show a fixed number of decimals (12.50), use string.format("%.2f", x). To round, see math.

Arithmetic

operatormeaningexampleresult
+addition64 + 1680
-subtraction (and -x, the negative of x)64 - 1648
*multiplication27 * 641728
/division, always with decimals7 / 23.5
//floor division: divide, then round down7 // 23
%remainder (modulo)7 % 21
^power2 ^ 101024

// and % work as a pair: 200 items are 200 // 64 full stacks and 200 % 64 items left over. With negative numbers, // rounds towards minus infinity and the result of % takes the sign of the divisor, so % is perfect for things that go round in a cycle (four lamps, the steps of a sequence, the hours of a day).

Brass
local items = 200
print(items // 64 .. " stacks and " .. items % 64 .. " items")
print(5 % 4, 8 % 4, -1 % 4)
print(-7 // 2, 2 ^ 0.5)
Screen
3 stacks and 8 items
1   0   3
-4  1.4142135623731

Strings

A string is a piece of text, between double quotes "..." or single quotes '...'. Both are the same: pick the one that lets you write the other inside without trouble ('Press "Start"').

Inside quotes, a backslash starts an escape sequence, a way to write a special character:

escapegives
\na new line
\ta tab: on screen, it moves to the next column that is a multiple of 4
\\a backslash
\" and \'a quote
\xNNthe character with the hexadecimal code NN: \x41 is A
\r, \0carriage return, null character (rarely useful)

Any other backslash sequence is a compile error (invalid escape sequence '\q'), and a string must end on the line it starts (unfinished string).

A long string sits between [[ and ]]. It can span several lines and has no escapes (backslashes stay as typed). A line break right after the [[ is skipped. It is handy for help screens and drawings:

Brass
print("Iron:\t64\nGold:\t8")
print('Press "Start"')
print([[
+--------+
| VAULT  |
+--------+]])
Screen
Iron:   64
Gold:   8
Press "Start"
+--------+
| VAULT  |
+--------+

Strings are joined with .. (see concatenation), and # gives their length: #"iron" is 4. A string never changes: functions such as string.upper return a new string. They can be called as methods, ("iron"):upper() or name:upper(). The Strings guide and the string reference cover them all.

Booleans and truth

A boolean is true or false. Comparisons produce them (stock < 64), and conditions (if, while, and, or, not) use them.

But a condition accepts any value, and the rule is short: **only false and nil are false. Everything else is true, including 0 and the empty string "".** This differs from C, JavaScript or Python, and it matters with redstone: rs.get returns a number from 0 to 15, so if rs.get("left") then is always true. Compare the number:

Brass
local signal = 0   -- what rs.get gives for an unpowered side
if signal then
  print("0 counts as true!")
end
if signal > 0 then
  print("powered")
else
  print("not powered")
end
Screen
0 counts as true!
not powered

nil

nil means "nothing here". A variable that was never given a value, a table field that does not exist, the result of a function that returns nothing: all are nil. Reading them is not an error; using them in a calculation is.

Brass
local machines = {press = 64}
print(machines.press, machines.mixer)
print(never_set)
Screen
64  nil
nil

Giving nil to a variable or a field erases its value. To test for it, write if x == nil then. The shorter if not x then also catches false, which is fine as long as false is not a meaningful value for x.

Variables

A variable is a name for a value. name = value stores the value; writing the name later reads it back.

  • A name is made of letters, digits and _, and does not start with a digit: stock, max_rpm, side2.
  • Case matters: Count and count are two different variables.
  • These words are reserved and cannot be names: and break do else elseif end false for function if in local nil not or repeat return then true until while.
  • Avoid the names of the libraries and built-in functions (table, string, type, print...): your variable would hide them.

Local and global

local x = 1 creates a local variable. It exists from that line to the end of the block it is written in: the file, a function, a loop, a branch of an if. A name declared again with local in an inner block is a new variable that hides the outer one until the end of that block:

Brass
local level = 1        -- the whole file sees this one
do
  local level = 2      -- another variable, only inside do ... end
  print("inside:", level)
end
print("outside:", level)
Screen
inside: 2
outside:    1

Without local, x = 1 writes a global variable. Globals are visible from every function and every file of the program, and they stay in the computer's memory after the program ends, until it reboots. Reading a global that does not exist gives nil.

**Use local by default.** A local cannot be changed by accident from another file or by the next program, and reading the code shows where it comes from. The locals of a function also vanish when the function returns, and their memory with them. Keep globals for the rare value that must be shared on purpose.

A local written at the top level of a file, outside any function, is a chunk-level local. Every function of that file written after it can read and change it: it is the right place for the state of your program (a counter, the settings at the top of the file). This matters in Brass because a function cannot use the locals of the function around it: see the scoping rule.

local x alone declares the variable with the value nil.

Multiple assignment

Several variables can be assigned in one line, separated by commas. Missing values give nil, extra values are dropped. All the values on the right are computed before any variable changes, which makes swapping two variables a one-liner:

Brass
local x, y, z = 120, 64, -35   -- a position
print(x, y, z)
local input, output = "lamp", "piston"
input, output = output, input  -- swap
print(input, output)
local a, b = 1
print(a, b)
Screen
120 64  -35
piston  lamp
1   nil

A function returns a single value in Brass, so local a, b = f() always leaves b at nil: a function that has several things to give back returns a table.

Operators

Comparison

operatortrue when
==the two values are equal
~= or !=they are different (both spellings work)
<, <=, >, >=less than, at most, greater than, at least
  • Numbers, strings and booleans are compared by value. Tables and functions are compared by identity: two tables are equal only if they are the same table (see references).
  • Values of different types are never equal: "10" == 10 is false.
  • <, <=, >, >= compare two numbers, or two strings in character order ("Z" < "a" is true: capitals come first). A number against a string is an error: attempt to compare number with string.
  • = stores, == compares. if x = 5 then is a compile error: 'then' expected near '='.
  • 1 < x < 10 does not mean what it looks like: 1 < x gives a boolean, then true < 10 is an error. Write 1 < x and x < 10.

Logic: and, or, not

expressionresult
not atrue if a is false or nil, else false
a and ba if a is false or nil, else b
a or ba if a is neither false nor nil, else b

and and or stop as soon as the answer is known, so the right side is only computed when needed: chest ~= nil and chest.count() > 0 never calls chest.count on a missing chest.

Because they return one of their two values (not just true or false), they give two very common idioms:

  • A default value: local side = wanted_side or "back".
  • A choice in one line: cond and x or y gives x when cond is true, else y. For example rs.set("top", full and 15 or 0).

The second idiom has a trap: when x itself is false or nil, the or takes over and you always get y.

Brass
local full = true
print(full and "FULL" or "ok")
local keep_closed = true
print(keep_closed and false or "open")  -- wanted false
Screen
FULL
open

When the middle value can be false or nil, write a real if ... else ... end (see Conditions and loops).

Concatenation and length

.. joins two strings into a new one. Numbers are turned into text on the way: "Stock: " .. 64 is "Stock: 64". Any other value (nil, a boolean, a table) is an error, attempt to concatenate a nil value: wrap it in tostring first. Leave spaces around .. next to numbers, it reads better: 1 .. 2 is "12".

# gives the length of a string (#"iron" is 4) or of a list (#{"a", "b"} is 2; see lists).

Precedence

When an expression mixes operators, the ones higher in this table are applied first. Operators on the same line are applied from left to right, except ^ and .., which go from right to left.

priorityoperators
highest^
not, #, - (in front of a value)
*, /, //, %
+, -
..
==, ~=, !=, <, <=, >, >=
and
lowestor
Brass
print(2 + 3 * 4, (2 + 3) * 4)
print(-2 ^ 2, 2 ^ 3 ^ 2)
print("total: " .. 60 + 4)
print(not 1 == 2)
Screen
14  20
-4  512
total: 64
false

The last two lines are traps: + comes before .., so 60 + 4 is computed first (which is what you want here), and not comes before ==, so not 1 == 2 is (not 1) == 2, that is false == 2. Write not (x == y), or simply x ~= y. When in doubt, add parentheses: they cost nothing.

Converting between types

Brass never turns text into a number on its own: "10" + 1 is an error (attempt to perform arithmetic on a string value), where Lua would give 11. The one automatic conversion goes the other way: .. accepts numbers.

Text typed by the player (read) or received from another computer is a string. Convert it before you compute with it, and handle the case where it is not a number:

Brass
local typed = "32"        -- what read() could return
local rpm = tonumber(typed)
if rpm == nil then
  print("please type a number")
else
  print("double speed: " .. rpm * 2 .. " RPM")
end
print(tonumber("12 RPM"), tonumber(" 12 "), tonumber("0x1F"))
Screen
double speed: 64 RPM
nil 12  31

Errors you will meet

When something goes wrong while the program runs, it stops and prints a red message: the file, the line, and what happened. Here the program is called snippet; yours shows the name of its file.

Brass
local stock
print(stock + 1)
Screen
snippet:2: attempt to perform arithmetic on a nil v
alue

(The screen of a Personal Computer is 51 characters wide, so a long message wraps.) The messages a beginner meets most:

messageusual causewhat to do
attempt to perform arithmetic on a nil valuea variable that was never given a value, a name misspelled, a missing table fieldcheck the spelling, give a start value (local count = 0)
attempt to perform arithmetic on a string valuetext in a calculation, like "10" + 1convert with tonumber
attempt to concatenate a nil value"Stock: " .. count while count is niltostring(count), or (count or 0)
attempt to call a nil value (global 'prnt')a misspelled function, or a function called before the line that defines itfix the name or the order
attempt to index a nil value (local 'chest')chest.count() while chest is nil, for example a block that is not theretest if chest == nil then first
attempt to compare number with stringtyped < 10 with a string from read()convert with tonumber
attempt to get length of a nil value#list while list is nilcheck where the list comes from

Mistakes in the code itself are found before anything runs, as compile errors: 'end' expected (to close 'if' at line 2) near <eof> (a missing end), 'then' expected near 'print', unfinished string, unexpected symbol '!' (use 'not'). The Errors guide explains how to read them, catch runtime errors with pcall and raise your own with error; the error reference lists every message.