Language tour

A guided pass through the language as it stands today. Every example in this page is valid Capa, accepted by the analyzer and runnable through the transpiler.

Hello, Capa

The smallest Capa program. Notice the stdio: Stdio parameter: Capa has no print built in. Standard output is a capability that main receives from the runtime, and only code that holds it can write.

// hello.capa
fun main(stdio: Stdio)
    stdio.print("Hello, world!")

Run it:

$ python -m capa --run hello.capa
Hello, world!

Types and values

Primitive types: Int, Float, Bool, String, Char, Unit. Built-in generic types: List<T>, Map<K, V>, Set<T>, Option<T>, Result<T, E>, Fun(...) -> .... Tuples too: (Int, String).

let n: Int = 42
let pi: Float = 3.14
let name: String = "Ana"
let flags: List<Bool> = [true, false, true]
let pair: (Int, String) = (1, "one")
let maybe: Option<Int> = Some(7)

String interpolation uses ${...}. Numeric literals support _ separators and bases (0x, 0o, 0b):

let mask = 0xff_ff
let who = "world"
stdio.println("hi, ${who}! mask=${mask}")

Variables and constants

Three bindings: const (compile-time, module-level), let (immutable local), var (mutable local). Type annotations are optional when inference can resolve them.

const VERSION: String = "0.1.0"

fun demo()
    let x = 10           // inferred Int, immutable
    var count = 0       // inferred Int, mutable
    count += 1
    count = count * 2

Functions

Functions take parameters, may return a value, and use indentation for the body (no braces around top-level blocks). The arrow -> declares the return type; functions without an arrow return Unit.

fun add(a: Int, b: Int) -> Int
    return a + b

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

Functions that touch system resources must declare those resources as parameters. A function with no capabilities in its signature is, by construction, pure with respect to the outside world.

Control flow

if / else if / else, while, and for ... in .... Loops can iterate over any iterable, including lists, ranges, and strings.

fun grade(n: Int) -> String
    if n >= 18
        return "Excellent"
    else if n >= 14
        return "Good"
    else if n >= 10
        return "Pass"
    else
        return "Fail"

fun count_up(stdio: Stdio)
    for i in 0..10
        stdio.println("i=${i}")

    for j in 0..=5
        stdio.println("j=${j}")

Ranges: a..b is exclusive, a..=b is inclusive. They are first-class values that can be stored, iterated, and passed around.

Pattern matching

match is exhaustive over the cases it covers; an underscore arm catches the rest. Patterns include literals, variant constructors with bindings, tuples, and nested forms. match can be used as a statement or as an expression.

type Shape =
    Circle(Float)
    Rect(Float, Float)
    Empty

fun area(s: Shape) -> Float
    match s
        Circle(r)    -> 3.14159 * r * r
        Rect(w, h)   -> w * h
        Empty        -> 0.0

fun label(stdio: Stdio, x: Option<Int>)
    match x
        Some(n) -> stdio.println("got ${n}")
        None    -> stdio.println("nothing")

Types: structs and variants

The type keyword defines a struct (named fields) or a sum-type (named variants, optionally with payloads). The same keyword covers both; Capa uses syntax, not separate keywords, to distinguish them.

Struct
type Point {
    x: Float,
    y: Float
}

let p = Point { x: 1.0, y: 2.0 }
Sum type (variants)
type Color =
    Red
    Green
    Blue
    Rgb(Int, Int, Int)

let c = Rgb(255, 128, 0)

Generics

Type parameters in angle brackets. The analyzer infers concrete types from call-sites, type annotations on call expressions are almost never necessary.

type Pair<A, B> {
    first: A,
    second: B
}

fun wrap<T>(x: T) -> Pair<T, T>
    return Pair { first: x, second: x }

let p1 = wrap(42)              // Pair<Int, Int>
let p2 = wrap("hello")         // Pair<String, String>

Closures

Anonymous functions are first-class values with the fun keyword and a => arrow into the body. Block-bodied lambdas are also supported.

fun apply(f: Fun(Int) -> Int, x: Int) -> Int
    return f(x)

fun main(stdio: Stdio)
    let double = fun (x: Int) -> Int => x * 2
    stdio.println("double(21) = ${double(21)}")

    let sq = apply(fun (x: Int) -> Int => x * x, 7)
    stdio.println("square(7) = ${sq}")

Errors are values

Capa has no exceptions in user code. Fallible operations return Result<T, E>. The ? operator unwraps an Ok or short-circuits the function with the Err, the same pattern as Rust.

fun process(fs: Fs, input: String) -> Result<Int, IoError>
    let payload = fs.read(input)?
    return Ok(payload.length())

fun main(stdio: Stdio, fs: Fs)
    match process(fs, "/tmp/data.txt")
        Ok(n)  -> stdio.println("read ${n} bytes")
        Err(e) -> stdio.eprintln("failed: ${e}")

Capabilities

This is what makes Capa Capa. Every system effect, IO, network, filesystem, environment, clock, randomness, is a value with a type, threaded through function signatures. The Why Capa page makes the case; this section is the reference.

Built-in capabilities

Seven, exposed by the runtime:

User-defined capabilities

A library declares its own capability with the capability keyword. The discipline applies uniformly, a user-defined capability behaves exactly like a built-in one, by construction.

capability SendEmail
    fun send(self,
        to: String,
        subject: String,
        body: String) -> Result<Unit, IoError>

type SmtpMailer {
    server: String,
    net: Net
}

impl SendEmail for SmtpMailer
    fun send(self, to: String, subject: String, body: String) -> Result<Unit, IoError>
        // implementation uses self.net
        return Ok(())

fun welcome(mailer: SendEmail, to: String) -> Result<Unit, IoError>
    return mailer.send(to, "Welcome", "Hello!")

A struct that implements a user-defined capability is allowed to hold built-in caps as fields (here, net: Net), this is the only relaxation of the structural rule, and it exists precisely so that high-level caps can encapsulate low-level ones.

Attenuation

Net.restrict_to(host) returns a fresh Net whose authority is narrowed to a single host. Restrictions only narrow, chaining two restrict_to calls intersects their allowed sets. The runtime checks the host before any system call.

fun probe(net: Net, stdio: Stdio)
    stdio.println("api allowed?  ${net.allows(\"api.example.com\")}")
    stdio.println("evil allowed? ${net.allows(\"evil.example.com\")}")

fun main(net: Net, stdio: Stdio)
    let api = net.restrict_to("api.example.com")
    probe(api, stdio)
    // `api` allows api.example.com only.
    // `evil.example.com` is blocked before any socket opens.

Attenuation is the lever that lets a function declare it needs the network and declare exactly which network. Without it, a capability would be all-or-nothing and the discipline would buy you less.

The pattern extends to the other built-in capabilities. Fs.restrict_to(prefix) returns a fresh Fs whose authority is narrowed to paths starting with the prefix; Env.restrict_to_keys([...]) returns a fresh Env that can only read the named variables. Chaining intersects, never widens.

fun main(fs: Fs, env: Env, stdio: Stdio)
    let app_fs  = fs.restrict_to("/tmp/myapp/")
    let app_env = env.restrict_to_keys(["HOME", "APP_TOKEN"])
    // `do_work` and anything it calls can only touch /tmp/myapp/
    // and the two env vars, no matter what its implementation tries.
    do_work(app_fs, app_env, stdio)

Denied operations are information-hiding by default: fs.exists on a denied path returns false, and env.get on a denied key returns None, the same answer a caller would get for a path that does not exist or a variable that is unset. The capability never leaks the existence of resources outside its allowed surface.

Clock also attenuates: clock.restrict_to_after(t) returns a Clock active only after the timestamp t (seconds since the epoch). Chained calls take the maximum threshold. clock.sleep(...) becomes a silent no-op on a denied Clock; clock.now_secs() stays ungated because reading the current time is a pure query.

Random.with_seed(seed) closes the arc with a different flavour: it returns a deterministic Random whose sequence is a function of the integer seed. There is no "denied" state, since generating a number is always permitted, but the audit value is real: the manifest sees that the RNG was made reproducible before being handed onward. Chained with_seed calls just re-seed (last wins).

The consume qualifier

A parameter prefixed with consume takes ownership: after the call returns, the argument can no longer be used by the caller. This is the linear layer of the discipline, useful for resources whose handover semantics matter (file handles being closed, capabilities being transferred to a long-lived owner, etc).

fun adopt(consume stdio: Stdio)
    stdio.println("kept forever by adopt")

fun borrow(stdio: Stdio)
    stdio.println("used and returned")

fun main(stdio: Stdio)
    borrow(stdio)
    borrow(stdio)
    adopt(stdio)
    // After adopt(stdio), `stdio` cannot be used here.
    // Adding another call below would be a compile error.

Fork/merge tracking handles branches: if both arms of an if consume the same value, the value is consumed after the if; if only one arm consumes it, the analyzer is conservative and treats it as consumed.

Attributes and the capability manifest

Functions can carry static, source-level metadata via attribute syntax that mirrors Python decorators or Rust attributes. v1 supports three: @security (link to a CVE or describe a known issue), @deprecated, and @audited. Each takes keyword-style string arguments validated against a fixed schema.

@security(
    cve: "CVE-2024-12345",
    severity: "high",
    fixed_in: "0.2.0"
)
@audited(date: "2026-05-11", by: "Nelson Duarte")
fun verify_token(token: String, expected: String) -> Bool
    return token == expected

The analyzer rejects unknown attribute names, unknown keys, and duplicates, so consumers can trust the schema.

The --manifest flag of the compiler emits a JSON description of the program: per function, the declared capabilities, the attached attributes, and whether the function crosses the Unsafe boundary; per module, the user-defined capabilities and their implementors, plus a summary count. This is the artefact an EU Cyber Resilience Act auditor wants, evidence of the program's authority graph that does not require running it.

$ python -m capa --manifest verify.capa
{
  "schema_version": 1,
  "functions": [
    {
      "name": "verify_token",
      "declared_capabilities": [],
      "has_unsafe": false,
      "attributes": [
        {"name": "security", "args": {"cve": "CVE-2024-12345", ...}},
        {"name": "audited",  "args": {"date": "2026-05-11", ...}}
      ],
      ...
    }
  ],
  "summary": {"total_functions": 1, "functions_with_capabilities": 0, ...}
}

No other mainstream language can emit this because the authority graph is not in their type system. In Capa, it falls out of the analyser for free.

Ready to try it?