Modules and Packages
So far every program has been one file. Real programs are split into modules, and use code that other people wrote. This chapter shows how files become modules, how pub decides what a module shares, and how tessera.toml brings in dependencies. It also shows the first place where capabilities pay off at the scale of a whole program: the compiler knows, from signatures alone, everything a dependency can do.
Modules
Every .tsr file is a module, named after the file. use NAME makes the module NAME available:
// Shapes on a plane.
pub struct Point {
x: Float,
y: Float,
}
// How far apart two points are.
pub fn distance(a: Point, b: Point) -> Float {
(square(a.x - b.x) + square(a.y - b.y)).sqrt()
}
pub fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}
// Only this module can call it.
fn square(x: Float) -> Float {
x * x
}use geometry
fn main(root: Root) {
let p = geometry.Point { x: 3.0, y: 4.0 }
let d = geometry.distance(geometry.origin(), p)
root.stdout.print("{p} is {d} from the origin")
}$ tessera run main.tsr
geometry.Point { x: 3.0, y: 4.0 } is 5.0 from the origin
use geometry looks for geometry.tsr next to the file, then for a dependency named geometry in tessera.toml, then in the standard library. The standard library's modules, such as text, json, http and time, come in the same way, with use text and so on. A file of your own with the same name as a standard module wins in your code, but never inside the standard library: when http uses url, it always gets the standard one, so naming a file url.tsr can't break it.
A module's items are reached through its name: geometry.distance(...), geometry.Point { ... }. That keeps every name's origin visible where it's used. Inside its own file, a module uses its items without the prefix.
The comment block at the top of a file, followed by a blank line, documents the module, and the comment directly above an item documents that item. tessera doc geometry.tsr turns them into documentation, as appendix B shows.
pub
Only items marked pub can be used from other modules. geometry exports Point, distance and origin, and keeps square to itself:
use geometry
fn main(root: Root) {
root.stdout.print("{geometry.square(3.0)}")
}$ tessera check main.tsr
error[E0402]: `square` is private to module `geometry`
--> main.tsr:4:34
|
4 | root.stdout.print("{geometry.square(3.0)}")
| ^^^^^^
= note: mark it `pub fn`, `pub struct` or `pub enum` in ./geometry.tsr to export it
1 error
A struct's fields and an enum's variants are visible wherever the type is. To share a method with other modules, mark it pub fn too.
Modules may use each other, even in a cycle. test blocks inside a module run only when you test that file, not when another file uses it.
Packages and dependencies
A package is a directory with a tessera.toml. The [dependencies] table lists other packages the program uses, and for each one, what it may do:
[package]
name = "hello"
[dependencies]
banner = { path = "vendor/banner", caps = ["stdout"] }// Prints text in a box.
pub fn show(out: Stdout, text: Str) {
let line = "+" + "-".repeat_n(text.len() + 2) + "+"
out.print(line)
out.print("| {text} |")
out.print(line)
}
fn repeat_n(self: Str, n: Int) -> Str {
let mut s = ""
for _ in 0..<n {
s = s + self
}
s
}use banner
fn main(root: Root) {
banner.show(root.stdout, "Hello, packages!")
}$ tessera run main.tsr
+------------------+
| Hello, packages! |
+------------------+
A dependency is a directory holding NAME.tsr (plus any modules that file uses). It comes from a local path, as here, or from a git repository at a tag or a commit:
[dependencies]
banner = { path = "vendor/banner", caps = ["stdout"] }
color = { git = "https://example.com/color.git", tag = "v1.2.0", caps = ["stdout"] }
yaml = { git = "[email protected]:me/yaml.git", commit = "3f9c2e1d7a" }
Git dependencies are fetched by the compiler into a cache, and their exact commit and a hash of their files go into tessera.lock, which you commit. Every later build uses the locked commit and checks the files against the hash, so a moved tag or a changed cache can't slip new code into your program. Only the commands that produce something, tessera build, tessera run and tessera test, write tessera.lock; tessera check and your editor read it and never change it, so checking a project leaves its files as they were. Each dependency, including the dependencies of your dependencies, is listed in your own tessera.toml.
What a dependency may do
Look again at caps = ["stdout"]. It says this dependency may receive the Stdout capability, and no other. This is where capabilities change how you use other people's code.
In most languages, any library you import can read your files, open network connections and run programs, from any function, at any time. You have to trust every line of every package in your dependency tree. In Tessera, a library has no way to reach the outside world except through capabilities your code passes to it. So the types of its functions say everything it can ever do, and the compiler checks those types against what tessera.toml allows.
Say banner publishes a new version that wants to report usage to a server:
// Prints text in a box (and, since version 2, reports home).
pub fn show(out: Stdout, net: Net, text: Str) {
let _ = net.get("http://stats.example.com/used?text={text}")
out.print("| {text} |")
}$ tessera check main.tsr
error[E0320]: dependency `banner` uses capability `Net`, which tessera.toml does not grant it
--> ./tessera.toml:5:1
|
5 | banner = { path = "vendor/banner", caps = ["stdout"] }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: `banner.show` takes `Net` (./vendor/banner/banner.tsr:3)
= note: to allow it, add "net" to its caps
1 error
The program doesn't build. The new version takes a Net, and nobody agreed to that. Adding "net" to its caps would be a decision you make on purpose, in a reviewed diff, and even then the dependency would get exactly the Net your code hands it, which can be limited to one host (chapter 12).
A few rules complete the picture:
capscan listnet,listen,fs,stdout,stderr,stdin,clock,rand,envandexec. The default is none: a dependency withoutcapscan only compute.- No dependency can take
Root. - A dependency's own tessera.toml never grants anything, and can't add directories to embed from (chapter 12). Only the program's tessera.toml does, so adding or updating a dependency can't widen what your program may do.
- Calling C is outside every capability check, so it is not one more cap. A dependency may call C only if the program names it under
[trust], as chapter 15 explains.
The standard library is checked the same way. json, text, time and the other pure modules may take nothing; http may take network capabilities; sql may take an Fs.
The package index
There's no central registry that stores packages. A package lives in its own git repository, and a package index is just a list of entries pointing at repositories, kept in a git repository or a directory of .toml files:
[[package]]
name = "color"
git = "https://example.com/color.git"
tag = "v1.2.0"
commit = "8d022128f5f177019f1e6aed46f75c93c90815f1"
description = "Colors for terminal output"
caps = ["stdout"]
[[package]]
name = "color"
git = "https://example.com/color.git"
tag = "v1.10.0"
commit = "41b07a5d0c3e9f8e2a6b1c4d7e8f9a0b1c2d3e4f"
description = "Colors for terminal output"
caps = ["stdout"]
[[package]]
name = "webhook"
git = "https://example.com/webhook.git"
tag = "v0.3.1"
commit = "c0ffee0123456789abcdef0123456789abcdef01"
description = "Send webhooks with retries"
caps = ["net", "clock"]Each entry names a version (a git tag), the commit it pointed at, and the capabilities the package needs. Set TESSERA_INDEX to an index, and tessera search finds packages in it:
$ TESSERA_INDEX=index tessera search
color v1.10.0: Colors for terminal output
caps: stdout
older: v1.2.0
webhook v0.3.1: Send webhooks with retries
caps: net, clock
$ TESSERA_INDEX=index tessera search color
color v1.10.0: Colors for terminal output
caps: stdout
older: v1.2.0
Versions compare as numbers, so v1.10.0 is newer than v1.2.0. The capabilities are right there in the search results, before you install anything.
tessera add color adds the latest version to your tessera.toml, with caps set to exactly what the index lists, and prints those capabilities, because they are what you are granting. Before writing anything it fetches the tag and refuses if the tag has moved away from the indexed commit, or if the code takes a capability the index doesn't list. tessera add [email protected] picks a version.
To publish a version of your own package, tag a commit and run tessera index entry at the top of the repository. It prints the entry for that tag, with the capabilities computed from the code; publishing is adding that entry to an index, for example in a pull request.
Summary
Files are modules, use brings them in, and pub decides what they share. tessera.toml lists dependencies, from paths or git, pinned in tessera.lock. Each dependency gets a list of capabilities it may receive, and the compiler checks every function it exports against that list, so a dependency can never do more than you agreed to. The index is a list of pointers, with capabilities shown before you install.
Next: what to do when things go wrong.