Language reference

Full specification of the syntax and semantics of the Capa language. For a guided introduction, see the language tour. For the built-in APIs, see the standard library page.

1. Lexical structure

1.1. Encoding

UTF-8 is required. Identifiers may contain any Unicode letter, digits, and _, but must start with a letter or _.

1.2. Comments

// Line comment (runs to the end of the line)
/// Doc comment (attaches to the next declaration)
/** Block doc comment (same role) */

Regular block comments /* ... */ are also accepted by the lexer (and ignored). Only the doc variants are attached to AST nodes.

1.3. Indentation

Capa is indentation-sensitive, à la Python. Implicit INDENT/DEDENT/NEWLINE tokens are produced by the lexer:

1.4. Implicit continuation by leading dot

For multi-line method chaining, a line beginning with . is treated as a continuation of the previous line:

let r = xs
    .filter(...)
    .map(...)
    .fold(...)

1.5. Keywords

fun let var if then elif else match while for in
break continue return import const type trait impl capability
true false and or not consume self Self
async await yield defer where mut

The last row lists reserved-for-future-use keywords. The lexer recognises them; the parser rejects their use.

1.6. Literals

TypeExamples
Integer42, -7, 0, 1_000_000, 0xff, 0o755, 0b1010
Float3.14, 2.0, 1e10
String"hello", "a\nb", "x = ${x}"
Char'a', '\n'
Booltrue, false
List[1, 2, 3], []
Tuple(1, "a"), (x,), ()
Rangea..b (exclusive), a..=b (inclusive)

1.7. Interpolated strings

${expr} inside a string literal is parsed as a Capa expression:

let n = 7
"value = ${n * 2}"  // "value = 14"
"len = ${xs.length()}"

$$ is the literal-$ escape. Nested string literals inside interpolation are not supported.

2. Type system

2.1. Primitive types

Int, Float, String, Bool, Char, Unit. See the standard library for the methods on each.

2.2. Compound types

ConstructSyntax
ListList<T>
Tuple(T1, T2, ..., Tn)
FunctionFun(T1, T2) -> Ret
MapMap<K, V>
SetSet<T>
OptionOption<T>
ResultResult<T, E>

2.3. User-defined types

Structs:

type Person { name: String, age: Int }

Sum types (nominal variants):

type Shape =
    Circle(Float)
    Rectangle(Float, Float)
    Square(Float)

Variants may have zero or more payloads. Variants without a payload (type X = A) are constants, used without ().

2.4. Generics

Functions and types can take type parameters delimited by <>:

fun first<T>(xs: List<T>) -> Option<T>
    return xs.first()

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

Local inference: the caller rarely needs to supply explicit args. first<Int>([1,2,3]) is equivalent to first([1,2,3]).

2.5. Cross-statement inference

let xs = [] produces List<TyVar>. The first use pins the type parameter:

let xs = []
xs.push(42)        // OK, infers List<Int>
xs.push("oops")    // error: expects Int, got String

TyVar sharing propagates through aliases (let ys = xs) and into calls to typed functions (process(xs) where process: List<Int> -> ...).

2.6. Compatibility

compatible(expected, actual) is structural with exceptions:

3. Statements

3.1. Bindings

let name = "Ana"               // immutable, type inferred
let age: Int = 30              // immutable, explicit type
var counter = 0                // mutable
counter = counter + 1          // assignment (only for var)

Pattern matching in bindings:

let (a, b) = pair()            // tuple destructuring
let Person { name, age } = p   // struct destructuring

3.2. Control flow

// if-statement
if cond
    body1
elif cond2
    body2
else
    body3

// while
while cond
    body

// for
for x in iter
    body

// match (statement)
match scrutinee
    pat1 -> body1
    pat2 -> body2

// match (expression, multi-line)
let r = match scrutinee
    pat1 -> expr1
    pat2 -> expr2

// match (expression, inline single-line)
let r = match scrutinee { pat1 -> expr1, pat2 -> expr2 }

// break / continue (only inside loops)
break
continue

// return
return                  // returns ()
return expr             // returns a value

3.3. Expressions as statements

Any expression can be a statement (value discarded):

stdio.println("hello")      // call with side effect
xs.push(42)                 // mutation
1 + 2                       // value discarded (valid but useless)

4. Expressions

4.1. Operators

In decreasing precedence:

OperatorDescription
() [] .Call, index, field access
not -Unary
* / %Multiplicative
+ -Additive
.. ..=Range
< <= > >= == !=Comparison
andShort-circuit conjunction
orShort-circuit disjunction
?Try (Err propagation)

4.2. if as an expression

let cat = if cond then e1 else e2

The then keyword is the discriminator: without it, if is a statement.

4.3. match as an expression

match is the same production whether used as a statement or as an expression: the value is consumed in expression position and discarded in statement position. Two surface forms exist:

// Multi-line (indented arms, expression OR block body)
let r = match scrutinee
    pat1 -> expr1
    pat2 -> expr2

// Inline (single-line, comma-separated, expression body only)
let r = match scrutinee { pat1 -> expr1, pat2 -> expr2 }

Both forms accept guards and or-patterns. All arms must produce compatible types.

The inline form's { ... } opens immediately after the scrutinee. This collides syntactically with the struct-literal heuristic; to force a struct literal as the scrutinee, wrap it in parentheses:

match (Point { x: 1.0, y: 2.0 })
    Point { x, y } -> stdio.println("${x}, ${y}")

4.4. Lambdas

fun (x: Int) -> Int => x * 2                    // single-expression
fun (x: Int) -> Int =>                          // block body
    let y = x * 2
    return y + 1
fun () -> Int => 42                             // no params
fun (a: Int, b: Int) -> Int => a + b            // multiple params

Lambdas capture the lexical environment. If a single-line lambda contains a nested match, the transpiler automatically promotes it to a nested function.

4.5. The ? operator

Propagates Err in functions that return Result:

fun read_two(fs: Fs) -> Result<(String, String), IoError>
    let a = fs.read("a")?      // if Err, returns immediately
    let b = fs.read("b")?
    return Ok((a, b))

5. Pattern matching

5.1. Available patterns

PatternSyntaxMatches
Wildcard_Any value
IdentifierxBinds to x
Literal42, "x", trueEquality
Variant without payloadNoneSingleton variant
Variant with payloadSome(x), Ok(v)Match + bind
StructPerson { name, age }Match + bind fields
Tuple(a, b), (x, _, z)Tuple of the same arity
Or-patterna | b | cAny alternative

5.2. Or-patterns with bindings

Each alternative can bind variables, provided all of them bind the same set of names with compatible types:

match op
    Add(n) | Sub(n) | Mul(n) -> n   // n is Int in all

5.3. Guards

match n
    x if x > 0 -> "positive"
    x if x < 0 -> "negative"
    _ -> "zero"

5.4. Exhaustiveness

The checker requires full coverage:

type Color = Red | Green | Blue

match c
    Red -> "r"
    Green -> "g"
    // error: missing variant Blue

5.5. Type-parameter substitution

match m.get(k) where m: Map<String, Int> infers Some(n) with n: Int, not n: T. The owner's type parameters are substituted by the scrutinee's type arguments.

6. Capabilities

6.1. What they are

Capabilities are primitive types representing access to system resources (Stdio, Fs, Env, Net, Clock, Random, Unsafe). They are only accessible via function parameters; there are no global instances.

6.2. The capability discipline (3 layers)

Structural (v1): capabilities cannot appear in struct fields, variant payloads, function return types, constants, let/var bindings, generic args, or tuples. They only flow through parameters. One relaxation: cap-bearing structs that implement a user-defined capability may hold built-in caps as fields.

Flow (v2):

Linearity (v3): the consume keyword indicates ownership transfer:

fun close(consume f: File)
    // f cannot be used after this call

"Consumed" variables are tracked across fork/merge in if/elif/else and match. In loops, the analysis uses dry-run + redo to discover consumes in the first iteration.

6.3. Capability in the signature

fun main(stdio: Stdio, fs: Fs)            // multiple
fun pure(x: Int) -> Int                   // no capabilities (pure)
fun with_consume(consume cap: MyCap)      // ownership transfer

6.4. Attenuation

Every built-in capability has an attenuator that returns a fresh, narrower instance:

CapabilityAttenuatorSemantics
Netrestrict_to(host: String)Allowed host set, monotonic intersection
Fsrestrict_to(prefix: String)Allowed path prefix, monotonic
Envrestrict_to_keys(keys: List<String>)Allowed key set, monotonic intersection
Clockrestrict_to_after(t: Float)Active only after timestamp
Randomwith_seed(seed: Int)Deterministic sequence (no denied state)

Attenuated capabilities are also recorded in the --manifest output via the args_flow field. See the manifest page.

7. Imports

import std.fmt                       // the whole module
import std.collections.HashMap       // a specific type
import "./local.capa" as utils       // local file with alias

In v1, top-level import is rejected by the analyzer. The module system is reserved for a future version; for now, all useful code comes from the global standard library.

For Python interop, use the typed builtins py_import(unsafe, name) and py_invoke(unsafe, callable, args); both require the Unsafe capability. See the standard library page.

8. The main program

The entry point is a function called main that may take one or more capabilities as parameters. The capabilities are instantiated by the runtime at boot:

fun main(stdio: Stdio, fs: Fs, env: Env)
    let argv = env.args()
    stdio.println("received ${argv.length()} arguments")

If main returns Result<(), E>, an Err causes a non-zero exit code.

9. Differences from Python

Capa transpiles to Python 3.10+, but the semantics differ:

CapaPython
Capabilities required for I/OGlobals such as print, open
Types checked at compile timeDuck typing
Exhaustive match checkedmatch at runtime, no exhaustiveness
Or-patterns with consistent bindingsOr-patterns without bindings
let x: List<Int> = [] validPython equivalent has no checks
Mutation only with var or consumeEverything mutable
Manifest, SBOM, doc emitted by compilerManual via external tools

10. Known limitations

For the full roadmap, see the roadmap page and the TODO.md.