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.
-- 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)27 stacks of iron ingot full: true
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.
> 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.
--[[
Mechanical Press line
lever on the left, alarm lamp on top
]]
local rpm = 64 -- speed of the press
-- print("debug: " .. rpm)
print(rpm)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:
| type | examples | used for |
|---|---|---|
nil | nil | "no value": a variable never set, a missing field, an empty slot |
boolean | true, false | yes or no: is the lever on, is the tank full |
number | 64, -3.5, 1e3, 0xFF | counts, speeds, coordinates, redstone strength |
string | "iron", 'top' | text: item names, sides, messages |
table | {1, 2, 3}, {rpm = 64} | lists and records, see Tables |
function | print, function(x) return x * 2 end | code you can call, see Functions |
type tells you the type of a value, as a string:
print(type(64), type("top"), type(true))
print(type(nil), type({}), type(print))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:
| written | value |
|---|---|
64, -12 | whole numbers |
3.5, .5 | decimals (the point is a dot, never a comma) |
1e3, 2.5e-3 | with an exponent: 1e3 is 1000, 2.5e-3 is 0.0025 |
0xFF | hexadecimal: 255 |
print(12, 3.5, .5, 1e3, 0xFF)
print(7 / 2, 10 / 2, 2 ^ 10)12 3.5 0.5 1000 255 3.5 5 1024
How numbers print
- A whole number prints **without
.0**:10 / 2shows5, not5.0. - At most 14 significant digits are shown:
1 / 3shows0.33333333333333,math.pishows3.1415926535898. - Very large or very small numbers use an exponent:
1e15shows1e+15,0.00001shows1e-05. - Dividing by zero is not an error. It gives
inf(infinity),-inf, ornan("not a number", for0 / 0), so check a divisor yourself when it can be zero.
print(1 / 3, 1e15, 2 ^ 53)
print(1 / 0, -1 / 0, 0 / 0)
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)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
| operator | meaning | example | result |
|---|---|---|---|
+ | addition | 64 + 16 | 80 |
- | subtraction (and -x, the negative of x) | 64 - 16 | 48 |
* | multiplication | 27 * 64 | 1728 |
/ | division, always with decimals | 7 / 2 | 3.5 |
// | floor division: divide, then round down | 7 // 2 | 3 |
% | remainder (modulo) | 7 % 2 | 1 |
^ | power | 2 ^ 10 | 1024 |
// 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).
local items = 200
print(items // 64 .. " stacks and " .. items % 64 .. " items")
print(5 % 4, 8 % 4, -1 % 4)
print(-7 // 2, 2 ^ 0.5)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:
| escape | gives |
|---|---|
\n | a new line |
\t | a tab: on screen, it moves to the next column that is a multiple of 4 |
\\ | a backslash |
\" and \' | a quote |
\xNN | the character with the hexadecimal code NN: \x41 is A |
\r, \0 | carriage 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:
print("Iron:\t64\nGold:\t8")
print('Press "Start"')
print([[
+--------+
| VAULT |
+--------+]])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:
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")
end0 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.
local machines = {press = 64}
print(machines.press, machines.mixer)
print(never_set)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:
Countandcountare 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:
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)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:
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)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
| operator | true 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" == 10isfalse. <,<=,>,>=compare two numbers, or two strings in character order ("Z" < "a"istrue: capitals come first). A number against a string is an error:attempt to compare number with string.=stores,==compares.if x = 5 thenis a compile error:'then' expected near '='.1 < x < 10does not mean what it looks like:1 < xgives a boolean, thentrue < 10is an error. Write1 < x and x < 10.
Logic: and, or, not
| expression | result |
|---|---|
not a | true if a is false or nil, else false |
a and b | a if a is false or nil, else b |
a or b | a 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 ygivesxwhencondis true, elsey. For examplers.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.
local full = true
print(full and "FULL" or "ok")
local keep_closed = true
print(keep_closed and false or "open") -- wanted falseFULL 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.
| priority | operators |
|---|---|
| highest | ^ |
not, #, - (in front of a value) | |
*, /, //, % | |
+, - | |
.. | |
==, ~=, !=, <, <=, >, >= | |
and | |
| lowest | or |
print(2 + 3 * 4, (2 + 3) * 4)
print(-2 ^ 2, 2 ^ 3 ^ 2)
print("total: " .. 60 + 4)
print(not 1 == 2)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.
tonumber(s)reads a number from a string and givesnilwhen the text is not a number. Spaces around the number are allowed, and so are decimals, exponents and0xhexadecimal. With a base,tonumber("ff", 16)is255andtonumber("101", 2)is5.tostring(v)turns any value into text:tostring(nil)is"nil",tostring(true)is"true",tostring(12.0)is"12". A table gives its address, liketable: 0x1b6d3586.
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:
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"))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.
local stock
print(stock + 1)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:
| message | usual cause | what to do |
|---|---|---|
attempt to perform arithmetic on a nil value | a variable that was never given a value, a name misspelled, a missing table field | check the spelling, give a start value (local count = 0) |
attempt to perform arithmetic on a string value | text in a calculation, like "10" + 1 | convert with tonumber |
attempt to concatenate a nil value | "Stock: " .. count while count is nil | tostring(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 it | fix 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 there | test if chest == nil then first |
attempt to compare number with string | typed < 10 with a string from read() | convert with tonumber |
attempt to get length of a nil value | #list while list is nil | check 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.