Tutorial
This is the whole language, in order, from nothing. Each step is a
file you can save and run with tau run. If you know any C-like
language you will be at home by the third section; if you know Go you are
already home.
Install
You need Go and GCC. The submodules matter: the VM links against them.
$ git clone --recurse-submodules https://github.com/NicoNex/tau
$ cd tau
$ make install
That installs into ~/.local: the binary in ~/.local/bin, the standard
library and the runtime bundled programs are built on in ~/.local/lib/tau.
Nothing needs root. For a system wide install, PREFIX=/usr/local sudo make install.
Check it:
$ tau version
$ tau help
Hello world
Save this as hello.tau:
println("Hello, world")
$ tau run hello.tau
Hello, world
tau hello.tau is short for tau run hello.tau, and a shebang line works too,
so a Tau file can be an executable script:
#!/usr/bin/env tau
println("Hello, world")
There is no main, no imports to declare, no boilerplate: a file is a program
and it runs top to bottom.
Values and operators
Tau is dynamically typed. Variables come into being by assignment, and type(x)
tells you what you have: int, float, string, bool, list, map,
object, bytes, closure, error, null.
# Two integers divide into an integer; a float on either side makes it a
# float division.
n = 42
half = n / 2
println(n, half, type(half), n / 5, n / 5.0)
# Octal and hexadecimal literals.
perm = 0644
mask = 0x1f
println(perm, mask, oct(perm), hex(mask))
# Booleans, null and conversions.
ok = true
println(!ok, int("7") + 1, float(3), string(255))
$ tau run values.tau
42 21 float
420 31 0o644 0x1f
false 8 3 255
Two things worth pinning down. Two integers divide into an integer, the way
they do in C and in Go: 7 / 2 is 3, and the remainder is dropped. A float on
either side makes it a float division — 7 / 2.0 is 3.5 — so float(a) / b
is how two integers give a fraction. Dividing an integer by zero is an error,
where a float division gives inf. And integer literals can be written in octal
(0644), hexadecimal (0x1f) or binary (0b1010), which is why file
permissions read the way you expect.
# Arithmetic. Two integers divide into an integer, the remainder is dropped.
println(7 + 2, 7 - 2, 7 * 2, 7 / 2, 7 / 2.0, 7 % 2)
# Comparison and logic.
println(1 < 2, 2 == 2.0, "a" != "b", true && false, true || false, !true)
# Bitwise, on integers.
println(0b1010 & 0b0110, 6 | 1, 6 ^ 3, 1 << 4, 32 >> 2)
# Increment, decrement, and the compound assignments.
n = 10
++n
n += 5
n *= 2
println(n)
println(type(7 / 2), type(7 / 2.0), float(7) / 2)
The precedence is C’s, trap included: & is looser than ==, so a & b == c
means a & (b == c) and wants parentheses.
Comments start with # and run to the end of the line. A newline ends a
statement; a ; lets you put several on one line. A list, a map, a call and a
parameter list may end with a comma, so a line can be added to one written over
several lines without touching the line above it.
Strings
Strings are byte indexed: s[0] is a one byte string, len(s) is a byte count,
and slice(s, start, end) cuts a piece out. Interpolation is written with
braces, and any expression can go inside them. To write a literal brace, double
it.
name = "Tau"
temp = 25
# Any expression can go inside the braces.
println("hello {name}, it is {if temp > 20 { \"hot\" } else { \"cold\" }}")
# A doubled brace is a literal one.
println("a block looks like {{ ... }}")
# Strings are byte indexed, and slice cuts them.
s = "gopher"
println(s[0], len(s), slice(s, 0, 2))
# Backticks make a raw string: no escapes, no interpolation.
println(`raw\n {not interpolated}`)
$ tau run strings.tau
hello Tau, it is hot
a block looks like { ... }
g 6 go
raw\n {not interpolated}
Backticks make a raw string: no escapes, no interpolation, newlines allowed. They are the right tool for regular expressions and JSON literals.
Lists, maps and objects
Three collections, no ceremony. Lists are ordered and grow with append, maps
take any hashable key, objects are bags of named fields you build at runtime.
xs = [1, 2, 3]
xs = append(xs, 4)
println(xs, xs[1], len(xs))
m = {"host": "localhost", "port": 8080}
m["tls"] = true
println(m["host"], keys(m))
delete(m, "tls")
println(len(keys(m)))
# Objects are made with new() and filled field by field.
Dog = fn(name, age) {
d = new()
d.Name = name
d.Age = age
d.HumanAge = fn() { d.Age * 7 }
return d
}
snuffles = Dog("Snuffles", 8)
println(snuffles.Name, snuffles.HumanAge())
$ tau run collections.tau
[1, 2, 3, 4] 2 4
localhost [host, tls, port]
2
Snuffles 56
new() returns an empty object, and you give it fields with the dot. A
constructor is just a function that fills one in and returns it — that is the
whole object system. There are no classes and no inheritance; keys(o) lists
the fields an object has, including the ones holding functions.
No self, no this
A method is a function stored in a field, and it reaches the object the same way any closure reaches what it captured: the constructor’s local variable is still there, so the method uses it by name.
# No self, no this: a method is a closure that captured the object.
Queue = fn() {
q = new()
q.items = []
q.Push = fn(x) { q.items = append(q.items, x) }
q.Pop = fn() {
if len(q.items) == 0 {
return null
}
first = q.items[0]
q.items = slice(q.items, 1, len(q.items))
return first
}
q.Len = fn() { len(q.items) }
return q
}
q = Queue()
q.Push("a")
q.Push("b")
# A method is an ordinary value: take it out of the object and it still works,
# because what it closed over is the object, not the call it was reached by.
pop = q.Pop
println(pop(), q.Len())
# And it can be replaced, without the language needing a word for it.
loud = q.Push
q.Push = fn(x) { loud(string(x) + "!") }
q.Push("c")
println(q.Pop(), q.Pop())
$ tau run methods.tau
a 1
b c!
Two things follow, and both are useful. A method is an ordinary value, so
pop = q.Pop keeps working once it is out of the object — there is no receiver
to lose. And a method can be replaced from the outside, with no keyword for it,
because assigning to a field is all it takes.
The cost of a self keyword is not the five characters. It is a second
mechanism, with its own rules about when it is bound and to what, sitting next
to the closures the language already has. Tau has the closures.
Functions and closures
fn makes a function, and functions are values like any other: store them, pass
them, return them. The last expression of a body is the result, so return is
only needed to leave early.
# The last expression of a function is its result.
add = fn(a, b) { a + b }
# Recursion, and an early return.
fib = fn(n) {
if n < 2 {
return n
}
fib(n - 1) + fib(n - 2)
}
println(add(9, 1), fib(20))
# Functions are values: pass them around, return them.
adder = fn(n) { fn(x) { x + n } }
add10 = adder(10)
println(add10(5))
# Closures capture free variables by value: n was 1 when twice was made.
n = 1
twice = fn(x) { x * n * 2 }
n = 100
println(twice(3))
Closures capture the free variables they use by value, at the moment the closure is built. This is the one rule that surprises people coming from JavaScript, and it is the reason a loop can hand every closure its own copy:
# A closure keeps a copy of the free variables it uses, taken when the
# closure is built: later changes to the original do not reach inside.
make = fn() {
n = 1
f = fn() { n }
n = 100
return f
}
println(make()())
# Which is why every closure made in a loop gets its own value.
multipliers = fn() {
fns = []
for i = 0; i < 3; ++i {
fns = append(fns, fn(x) { x * i })
}
return fns
}
fns = multipliers()
println(fns[0](10), fns[1](10), fns[2](10))
$ tau run closures.tau
1
0 10 20
Control flow
if is an expression: it produces the value of the branch that ran, so you can
assign it. for comes in three shapes and that is all the looping there is —
there is no range-for.
a = 3
b = 7
# if is an expression, it gives back a value.
min = if a < b { a } else { b }
println(min)
# The three shapes of for.
for i = 0; i < 3; ++i {
println("i is {i}")
}
xs = [0, 1, 2, 3]
for len(xs) > 2 {
xs = slice(xs, 1, len(xs))
}
println(xs)
i = 0
for {
++i
if i == 5 {
break
}
}
println(i)
$ tau run control.tau
3
i is 0
i is 1
i is 2
[2, 3]
5
break and continue work as expected. The for x = recv(p) { ... } form you
will meet in the concurrency section is the second shape: the condition is an
assignment, and the loop stops when it yields null.
Errors
An error is a value. error("...") builds one, returning it is how a function
reports failure, and failed(x) says whether a value is one. There is nothing
to catch, and nothing unwinds behind your back.
The idiom is to put the assignment inside the check, so the value is bound either way:
div = fn(n, d) {
if d == 0 {
return error("division by zero")
}
n / d
}
# The assignment goes inside failed(), so the value is there either way.
if failed(res = div(16, 2)) {
println("boom: {res}")
} else {
println("16 / 2 is {res}")
}
if failed(res = div(1, 0)) {
println("boom: {res}")
}
$ tau run errors.tau
16 / 2 is 8
boom: division by zero
You will see this shape all through the standard library:
if failed(f = os.Open(path, null, null)) {
return f
}
Read it as “do the call, bind the result, and if it went wrong hand the error
back up”. exit(err) ends the program with that message; the errors module
adds errors.Wrap(err, "context") when you want to say where it happened.
Modules
A module is a file. import("name") evaluates it once and gives you back an
object holding its exported names. Names starting with a capital letter are
exported; lowercase ones stay private, exactly like Go, and the rule applies
to object fields too.
# Only the names starting with a capital letter leave the module.
prefix = "hello"
Greet = fn(name) { "{prefix}, {name}" }
Version = "1.0"
greet = import("greet")
strings = import("strings")
println(greet.Greet("world"))
println("greeter v{greet.Version}")
println(strings.ToUpper(greet.Greet("tau")))
$ tau run main.tau
hello, world
greeter v1.0
HELLO, TAU
Names are resolved relative to the importing file first, then against the
directories in TAUPATH, then against the installed standard library. Which
also means a file of yours called strings.tau will shadow the stdlib module of
that name for its neighbours — name your files with that in mind.
A module is a file, or a directory holding several: the files of a directory
share one scope and their capitalised names are what it exports, so a growing
module splits across files without any of them having to export to the others.
A path with slashes reaches into a directory — import("crypto/sha256") — and
a path whose first element is a host, import("github.com/you/thing"), is a
module fetched from elsewhere. That last part, with tau.mod, tau get and
versioning, is its own story in Tooling.
Concurrency
Put tau in front of a call and it runs as a routine, concurrently with the
rest of the program. Routines are cheap; spawn them freely.
Routines talk over pipes. pipe() makes one, send(p, x) puts a value in,
recv(p) takes one out and sleeps until there is one, close(p) shuts it down.
A recv on a closed pipe returns null, which is what ends a for loop over a
pipe. pipe(n) makes a buffered pipe of capacity n.
# A pipe is a FIFO queue shared by routines. recv sleeps until something
# arrives, and returns null once the pipe is closed, which ends the loop.
listen = fn(p, done) {
for val = recv(p) {
println("got {val}")
}
println("pipe closed, bye")
send(done, true)
}
p = pipe()
done = pipe()
tau listen(p, done)
send(p, "hello")
send(p, "world")
send(p, 123)
close(p)
# Wait for the routine to finish before the program ends.
recv(done)
close(done)
$ tau run pipes.tau
got hello
got world
got 123
pipe closed, bye
Notice the done pipe: when the program’s main routine reaches the end of the
file, the process stops. Waiting on a pipe is how you keep it alive until the
work is finished — pipes are the synchronisation primitive as well as the
channel.
A worker pool is then four lines. Three routines share one job pipe, and the values come back in whatever order they finish:
time = import("time")
# A routine is an ordinary call with 'tau' in front of it.
worker = fn(id, jobs, results) {
for job = recv(jobs) {
time.Sleep(10)
send(results, "worker {id} did job {job}")
}
}
jobs = pipe()
results = pipe()
for w = 1; w <= 3; ++w {
tau worker(w, jobs, results)
}
for j = 1; j <= 6; ++j {
send(jobs, j)
}
close(jobs)
# recv blocks until something shows up.
for i = 0; i < 6; ++i {
println(recv(results))
}
close(results)
$ tau run workers.tau
worker 1 did job 1
worker 3 did job 3
worker 2 did job 2
worker 1 did job 4
worker 3 did job 5
worker 2 did job 6
Files
The os module opens, reads and writes; bufio wraps a file for line by line
reading and buffered writing. os.Open(path, flags, perm) takes null for the
flags and permissions you do not care about, and returns an error value you
check with failed.
os = import("os")
bufio = import("bufio")
path = "/tmp/tau-notes.txt"
if failed(err = os.WriteFile(path, "alpha\nbeta\ngamma\n")) {
exit(err)
}
if failed(text = os.ReadFileString(path)) {
exit(text)
}
println("{len(text)} bytes")
# Line by line, through a buffered scanner.
if failed(f = os.Open(path, null, null)) {
exit(f)
}
s = bufio.NewScanner(f)
for s.Scan() {
println("line: {s.Text()}")
}
f.Close()
os.Remove(path)
$ tau run files.tau
17 bytes
line: alpha
line: beta
line: gamma
os.Create(path) is os.Open with the usual creating flags and mode 0644.
Files are objects: f.Read(n), f.ReadAll(), f.ReadString(), f.Write(data),
f.Seek(offset, whence), f.Close(). Anything with a Read is a reader and
anything with a Write is a writer, which is all io and bufio ask for.
Tests
Test files end in _test.tau and tau test runs them, each in its own process.
A test file hands testing.Main a list of [name, function] pairs; the t it
passes in carries the assertions.
Double = fn(x) { x * 2 }
Div = fn(n, d) {
if d == 0 {
return error("division by zero")
}
# float(n) and not n, or two integers would divide into an integer.
float(n) / d
}
testing = import("testing")
mathx = import("mathx")
testing.Main([
["Double", fn(t) {
t.AssertEq(mathx.Double(21), 42)
}],
["Div", fn(t) {
t.AssertEq(mathx.Div(9, 2), 4.5)
t.AssertError(mathx.Div(1, 0))
}]
])
$ tau test .
=== mathx_test.tau
--- PASS: Double (0ms)
--- PASS: Div (0ms)
ok 2 passed of 2 (0ms)
ok 1 test files passed
t.Assert(cond, msg), t.AssertEq(got, want), t.AssertNe(got, want),
t.AssertError(v), t.Error(msg), t.Fatal(msg) and t.Skip(msg) are the
whole vocabulary. A failing case reports the file and line and the process
exits non-zero, so it drops straight into CI.
Putting it together
Read a file, split it into words, count them, and print the three most frequent. Everything in it has appeared above.
os = import("os")
strings = import("strings")
list = import("list")
path = "/tmp/tau-wordcount.txt"
if failed(err = os.WriteFile(path, "the quick brown fox jumps over the lazy dog the end\n")) {
exit(err)
}
if failed(text = os.ReadFileString(path)) {
exit(text)
}
counts = {}
list.Each(strings.Fields(text), fn(w) {
if counts[w] == null {
counts[w] = 0
}
counts[w] = counts[w] + 1
})
words = list.Sort(keys(counts), fn(a, b) {
if counts[a] == counts[b] {
return a < b
}
counts[a] > counts[b]
})
for i = 0; i < 3; ++i {
println("{counts[words[i]]} {words[i]}")
}
os.Remove(path)
$ tau run wordcount.tau
3 the
1 brown
1 dog
From here the standard library is the next stop, and tooling covers building, formatting and the REPL.