The Tessera Book
Chapter 1

Getting Started

Let's install the compiler, write the traditional first program, and look at the handful of commands you will use every day.

Installing the compiler

Tessera is built from source. The compiler is written in OCaml and needs nothing beyond the standard OCaml distribution and dune, its build tool. To compile programs to native code you also need a C compiler, which you almost certainly have already. OpenSSL is optional: without it everything works except TLS. SQLite is needed only by the sql module, which chapter 14 uses; macOS ships it.

On macOS with Homebrew:

$ brew install ocaml dune openssl@3

On Debian 13 or later, which packages OCaml 5:

$ sudo apt install ocaml-nox ocaml-dune gcc libssl-dev libsqlite3-dev

Then, in a copy of the Tessera repository:

$ dune build
$ dune test

dune build leaves the compiler at _build/default/bin/main.exe. dune install copies it into your OCaml environment as tessera; or, while you try things out, an alias does the job:

$ alias tessera="$PWD/_build/default/bin/main.exe"

Run tessera with no arguments to check it works. It prints a summary of every command.

Hello, world

Make a directory for your projects, and in it a file called main.tsr. Tessera source files end in .tsr.

fn main(root: Root) {
    root.stdout.print("Hello, world!")
}
Listing 1-1: A program that prints a greeting examples/ch01-01-hello/main.tsr

Run it with tessera run:

$ tessera run main.tsr
Hello, world!

There is a lot in those three lines, so let's go through them.

fn main(root: Root) declares the function where the program starts. It takes one parameter, root, of type Root. Parameters are always written name: Type, and the body of a function goes between braces.

root is the most important value in any Tessera program. It holds every capability the program was given when it started: one to write to standard output, one for standard error, one to read standard input, one for the clock, one for random numbers, and others for files, the network, environment variables and running programs, if you grant them. Nothing else in the language can print, read or connect. If a function wants to do any of those, it has to be handed the capability for it.

root.stdout is the capability for standard output, a value of type Stdout, and print is one of its methods: it writes a string and a newline. Its sibling root.stderr, of type Stderr, has the same print and write methods and writes to standard error, the place for warnings and error messages; chapter 8 shows it at work.

There are no semicolons at the ends of lines. Statements end at a newline; you only need ; to put two of them on one line.

Running, checking and building

tessera run type-checks the program and then runs it in the interpreter. It is the quickest way to try something. Two other commands will be just as common:

$ tessera run main.tsr
Hello, world!
$ tessera check main.tsr
ok
$ tessera build main.tsr -o hello
$ ./hello
Hello, world!

tessera check only type-checks, and prints ok when there is nothing to report. It is what your editor runs on every keystroke, and what you run when you want to know whether a change compiles without running it.

tessera build compiles the program to a native executable. It generates C, hands it to your C compiler, and writes the binary you name with -o. The binary needs nothing else to run: no interpreter and no runtime library besides libc. Building caches its results, so building an unchanged program again takes a few milliseconds.

The interpreter and the native code behave the same way: same output, same errors, same order of events in concurrent programs. Use whichever is convenient. tessera run --native builds and runs in one step.

A project with tessera.toml

A single file is a fine program. Once a program grows, or needs settings of its own, give it a tessera.toml in its directory. Here is a small one:

[package]
name = "greeter"
description = "Says hello to whoever NAME names"

[grants]
env = ["NAME"]
examples/ch01-02-project/tessera.toml

The [package] table names the project. The [grants] table is more interesting. It lists capabilities the program receives every time you tessera run it, so you don't have to type them on the command line. This program reads one environment variable:

// Greets whoever the NAME environment variable names.
fn main(root: Root) {
    let name = root.env.get("NAME") ?? "world"
    root.stdout.print("Hello, {name}!")
}
Listing 1-2: Reading an environment variable examples/ch01-02-project/main.tsr

root.env.get("NAME") asks for the variable NAME. It might not be set, so the result is not a Str but an Option[Str]: either Some with the text, or None. The ?? operator takes the value out of an Option, or gives the default on its right when there is none. Chapter 4 covers Option properly.

The string "Hello, {name}!" shows interpolation: anything in braces inside a string is evaluated and turned into text.

$ tessera run main.tsr
Hello, world!
$ NAME=Ada tessera run main.tsr
Hello, Ada!

Now try the same program without the tessera.toml next to it:

$ NAME=Ada tessera run main.tsr
Hello, world!
$ NAME=Ada tessera run main.tsr --grant env=NAME
Hello, Ada!

Without a grant, root.env still exists, but it shows the program no variables at all, so NAME reads as missing. Granting it on the command line with --grant env=NAME, or in tessera.toml, is what lets the program see it. This is the first glimpse of the idea the whole language is built on, and we'll keep coming back to it.

Summary

You can now build the compiler, run a program in the interpreter with tessera run, check it with tessera check, and compile it with tessera build. You've seen that a program starts from root, and that anything beyond printing (to standard output or standard error), reading input, the clock and random numbers has to be granted. In the next chapter we'll write a real program: a guessing game.