Learn · Chapter 8 of 12

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:

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 that single value is the only way anything downstream of main can write to standard output.

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

# 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 ambient authority. 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; code that does not receive one cannot use it.

The full set of built-in capabilities

Seven capability types are baked into the language:

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

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

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

A function that takes no capabilities cannot do I/O. Not "is asked not to"; cannot: the names of the capability values are not in scope, and there is no way to obtain one out of thin air. The compiler enforces 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:

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 tells you the exact authority surface: a function whose signature contains no capabilities is provably pure.

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

fun main(stdio: Stdio, fs: Fs)
    match fs.read_to_string("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 enforces three rules that distinguish them. None of them are arbitrary; each one closes a specific hole that ambient authority leaves open.

  1. No aliasing. A capability cannot be bound to a let. let copy = stdio is a compile error. If you could alias a cap, code could squirrel it away in a global and bypass the signature-as-contract.

  2. No returning, no storing. A built-in capability cannot be returned from a function, stored as a struct field, or captured by a lambda. Authority flows downward through parameters; it cannot flow upward or sideways. (User-defined capabilities, chapter 10, relax this for the wrapper pattern; built-ins do not.)

  3. Must be used. A function that declares a capability parameter and never invokes a method on it is a compile error. The signature is a contract; declaring a cap you do not use is a misleading contract.

Together, the three rules guarantee that the set of capabilities in a function's signature is an upper bound on what the function can actually do. The manifest you saw in chapter 1 (and that the compiler emits at any time with capa --manifest) is just this fact written out.

The two error modes

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

fun main(stdio: Stdio)
    let contents = fs.read_to_string("hello.txt")
error: undefined name 'fs'
   2 |     let contents = fs.read_to_string("hello.txt")
                          ^

The name fs is not in scope. Add it to main's parameters: fun main(stdio: Stdio, fs: Fs).

Second: trying to alias a capability.

fun main(stdio: Stdio)
    let dup = stdio
    dup.println("hi")
error: 'stdio' is a capability and cannot be bound to a 'let'
       (capabilities flow through function parameters only)
   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_to_string 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.