Learn · Chapter 8 of 14

Your first capability

You have been using one since chapter 1. This chapter is where capabilities stop being a magic word in main and become a tool you reason about explicitly.

The thing you have already been doing

Every program in this tutorial has looked like this:

capa
fun main(stdio: Stdio)
    stdio.println("...")

The stdio parameter is a capability. The runtime hands a fresh Stdio value to main when the program starts, and the program prints through that value.

To see why this is unusual, look at the same idea in Python:

capa
# Python
def main():
    print("Hello")

The Python main takes no parameters and yet has access to print. The standard output stream is ambient authority: it is in the surrounding environment, available to any code anywhere in the process, without anything in the function's signature indicating that fact. The same is true of open, os.environ, urllib.request.urlopen, and every other I/O primitive.

Capa removes that ambient access for its built-in capabilities. The standard output stream becomes a value: a Stdio instance, no different at the language level from an Int or a String. The runtime gives one to main, and the compiler refuses a call on stdio in a function that does not have it in scope.

The full set of built-in capabilities

Ten capability types are baked into the language:

  • Stdio · standard output and input. println, print, eprintln, read_line.
  • Fs · the filesystem. read, write, exists, list_dir.
  • Net · the network. get, post (HTTP).
  • Env · environment variables. get, args.
  • Proc · subprocesses. Spawning and running external programs.
  • Clock · wall-clock time. now_secs, sleep.
  • Random · non-deterministic randomness. float_unit, int_range, with_seed.
  • Db · database access. Queries against a connected database.
  • Serve · inbound connections. listen, accept, recv, send, close.
  • Unsafe · the escape hatch. Anything that crosses into Python interop.

Serve is the odd one out, and deliberately so: every other capability reaches out, while Serve is the one that is reached. It is also the one capability that does not run on the Wasm backends. Binding a listening socket needs wasi:sockets, which is neither vendored in the compiler nor reachable from the wasmtime bindings the Wasm hosts are built on, so capa --wasm and capa --wit refuse any program whose signatures reach it, naming every offending function. Use the Python backend (capa --run / capa --check) for those.

Any function that needs one of these declares it as a parameter:

capa
fun read_config(fs: Fs) -> Result<String, IoError>
    return fs.read("config.txt")

fun fetch_users(net: Net) -> Result<String, IoError>
    return net.get("https://api.example.com/users")

A call on a capability that is not in scope is refused. A top-level fun double(x: Int) -> Int takes only an Int, so a stdio.println(...) in its body would be refused: there is no stdio for it to call. Not "is asked not to"; refused: the name is not in scope, and no constructor or literal produces a built-in capability. The compiler checks this structurally.

How a capability gets to where it is needed

The runtime gives the full set of capabilities to main. From main down, capabilities flow as parameters:

capa
fun greet(stdio: Stdio, name: String)
    stdio.println("Hello, ${name}")

fun greet_all(stdio: Stdio, names: List<String>)
    for name in names
        greet(stdio, name)

fun main(stdio: Stdio)
    greet_all(stdio, ["Ana", "Bruno", "Carla"])

Both intermediate functions declare Stdio because they ultimately call println or call a function that does. Reading the signatures of a Capa program shows the capabilities each function is handed as parameters.

When main needs more than one capability, list them as additional parameters:

capa
fun main(stdio: Stdio, fs: Fs)
    match fs.read("hello.txt")
        Ok(text) ->
            stdio.println(text)
        Err(e) ->
            stdio.eprintln("error: ${e}")

The runtime instantiates each parameter from its type: Stdio becomes a fresh Stdio, Fs a fresh Fs, and so on.

The discipline, in three rules

Capabilities are not normal values. The analyzer checks three rules that distinguish them.

  1. No copy of a parameter. A let whose right-hand side is a capability parameter is refused: let copy = stdio is a compile error (the second error mode below shows it).

  2. Not in fields, payloads or constants. The analyzer refuses a built-in capability type as the declared type of a struct field, a variant payload or a constant. (User-defined capabilities, chapter 10, relax this for the wrapper pattern: a struct that implements one may hold built-in capabilities other than Unsafe as bare fields.)

    A lambda can capture a capability, and that is a real limit worth knowing. A closure of type Fun(Request) -> Response can hold a captured Net and its type will not say so. The analyzer refuses a closure that consumes a captured capability, since it may run more than once. A function whose signature takes a function value is marked in the manifest as not provable from its types; following what such a closure carries is then a job for capa --manifest or a package capability ceiling, not for the type of the closure.

  3. Must be used. A capability parameter the function never uses is a compile error; prefix its name with _ to silence the check. The signature is a contract; declaring a cap you do not use is a misleading contract.

Together with the scope rule (a call on a capability that is not in scope is refused), these rules tie a call on a built-in capability to a value of it in the calling function's scope. The manifest you saw in chapter 1 (and that the compiler emits at any time with capa --manifest) records the capabilities each signature holds.

The two error modes

First: trying to use a capability you did not declare.

capa
fun main(stdio: Stdio)
    let contents = fs.read("hello.txt")
capa
error: undefined name 'fs'
   2 |     let contents = fs.read("hello.txt")
                          ^
error: capability parameter 'stdio' is declared but never used; prefix the name with '_' to silence this check
   1 | fun main(stdio: Stdio)
                ^

The name fs is not in scope. Add it to main's parameters: fun main(stdio: Stdio, fs: Fs). This fragment also trips the chapter-1 "declared but never used" rule for stdio, since it never prints; the two errors are independent.

Second: trying to alias a capability.

capa
fun main(stdio: Stdio)
    let dup = stdio
    dup.println("hi")
capa
error: capability 'Stdio' cannot appear in a 'let' binding; capabilities only flow through function parameters
   2 |     let dup = stdio
           ^

If you want a copy with narrower authority, use restrict_to (chapter 9). If you want to pass the capability into a helper function, do so directly: helper(stdio) instead of binding-then-passing.

Try this. Write a program that reads hello.txt and prints its length. main takes stdio: Stdio and fs: Fs; the body uses ? on the fs.read call to propagate failure, then prints the string's length.

Where you are now

You understand what a capability is and how authority flows through a Capa program. The next chapter is about narrowing that authority before passing it on: a function that ultimately calls one specific URL does not need full Net authority. restrict_to is the tool.

Keep going