Language reference
Full specification of the syntax and semantics of Capa. For a guided introduction see the Learn track; for the built-in APIs see the standard library.
1Lexical structure
Encoding. UTF-8 required. Identifiers may contain any Unicode letter, digit and _, but must start with a letter or _. Indentation is significant, à la Python: implicit INDENT / DEDENT / NEWLINE tokens; inside ( [ { a newline is suppressed. A line beginning with . continues the previous one (method chaining).
// Line comment
/// Doc comment (attaches to the next declaration)
/** Block doc comment (same role) */Keywords: fun let var if then elif else match while for in break continue return import as const type trait impl capability pub true false and or not consume self Self extern typestate linear become. Reserved for future use: async await yield defer where mut.
| Type | Examples |
|---|---|
| Integer | 42, 1_000_000, 0xff, 0o755, 0b1010 |
| Float | 3.14, 2.0, 1e10 |
| String | "hello", "a\nb", "x = ${x}" |
| Char / Bool | 'a', '\n' · true, false |
| List / Tuple | [1, 2, 3] · (1, "a"), () |
| Range | a..b (exclusive), a..=b (inclusive) |
Interpolation: ${expr} inside a string is parsed as a Capa expression; $$ is a literal $. Nested string literals inside interpolation are not supported.
2Type system
Primitives: Int, Float, String, Bool, Char, Unit. Compound: List<T>, tuples, Fun(T1,T2)->Ret, Map<K,V>, Set<T>, Option<T>, Result<T,E>.
type Person { name: String, age: Int }
type Shape =
Circle(Float)
Rectangle(Float, Float)
Square(Float)
fun first<T>(xs: List<T>) -> Option<T>
return xs.first()Variants without a payload are constants, used without (). Ok, Err, Some, None are reserved. Generics use local inference, so the caller rarely supplies explicit args. Cross-statement inference: let xs = [] is List<TyVar> and the first use pins the type. TyUnknown and TyVar are compatible with any type.
Linear types
A linear type is a must-consume struct: a value of it has to be used (consumed) exactly once. It cannot be silently dropped or aliased into two live copies, the same ownership discipline capabilities enjoy, applied to ordinary data. Use it for a resource whose closing or handing-off must not be forgotten (a handle, a receipt, a one-shot token).
linear type Ticket { id: Int }Typestates
A typestate declaration names a type together with the finite set of named states a value of it moves through, one per indented line; the optional { … } block is the data every state carries. A transition is written become(value, State), where the target is a bare state name. This lets the type track, for example, a claim lifecycle (Created → Connected → Closed) so an operation valid only in one state cannot be reached from another.
typestate Socket { fd: Int }
Created
Connected
Closed3Statements
let name = "Ana" # immutable, inferred
let age: Int = 30 # explicit type
var counter = 0 # mutable
let (a, b) = pair() # tuple destructuring
if cond
body1
elif cond2
body2
else
body3
for x in iter
body
match scrutinee
pat1 -> body1
pat2 -> body2Assignment is only valid for var. Any expression can stand as a statement (its value is discarded).
4Expressions
Operator precedence (decreasing): call / index / field . () []; unary not -; * / %; + -; range .. ..=; comparison; and; or; try ?.
# if as expression: `then` is the discriminator
let cat = if cond then e1 else e2
# match expression, inline form
let r = match scrutinee { pat1 -> e1, pat2 -> e2 }
# lambdas
fun (x: Int) -> Int => x * 2
# the ? operator propagates Err
let a = fs.read("a")?5Pattern matching
| Pattern | Syntax | Matches |
|---|---|---|
| Wildcard / Identifier | _ · x | Any value · binds to x |
| Literal | 42, "x", true | Equality |
| Variant | None · Some(x) | Singleton · match + bind |
| Struct / Tuple | Person { name, age } · (a, b) | Match + bind |
| Or-pattern | a | b | c | Any alternative |
Or-patterns may bind, provided every alternative binds the same names with compatible types (Add(n) | Sub(n) -> n). Guards: x if x > 0 -> …. Exhaustiveness is checked: every variant of a sum type, or a catch-all _.
6Capabilities
Capabilities are primitive types representing access to system resources (Stdio, Fs, Net, Env, Proc, Clock, Random, Db, Unsafe). They are only accessible via function parameters; there are no global instances. The discipline has three layers:
- Structural. Capabilities cannot appear in struct fields, variant payloads, return types, constants, bindings, generic args or tuples, only parameters. One relaxation: cap-bearing structs implementing a user-defined capability may hold built-in caps as fields.
- Flow. No aliasing (the same cap cannot fill two arg slots of one call); capability parameters must be used (or prefixed _).
- Linearity. consume marks ownership transfer; consumed variables are tracked across fork/merge in if and match.
Attenuation: every built-in capability has an attenuator returning a fresh, narrower instance:
| Capability | Attenuator | Semantics |
|---|---|---|
| Net | restrict_to(host) | Allowed host set, monotonic intersection |
| Fs | restrict_to(prefix) | Allowed path prefix, monotonic |
| Env | restrict_to_keys(keys) | Allowed key set, monotonic intersection |
| Clock | restrict_to_after(t) | Active only after timestamp |
| Random | with_seed(seed) | Deterministic sequence |
Foreign components
An extern component declaration types an external WebAssembly Component-Model component and the methods called across the boundary. component and from are contextual (they stay usable as ordinary identifiers); only extern is reserved. Each call is capability-checked at the boundary exactly like an ordinary function, and at runtime the Wasm sandbox confines the component to precisely the capability set its signatures declare. Unsafe cannot be a foreign-component parameter (the compiler rejects it at --check): the Python-only FFI escape hatch can never reach a sandboxed component. A resource ceiling (CPU and memory) is applied per call with --foreign-fuel / --foreign-memory-cap.
extern component Bureau from "vendor/bureau.wasm"
fun submit(net: Net, payload: String) -> String7Information-flow control
A two-point lattice: @public (default) below @secret. A label attaches to a type expression, so it can appear on parameters, bindings, return types and struct fields. A value's label is the join of every label flowing into it (operators, interpolation, field reads, indexing, ?). env.get is secret by default; fs.read is intentionally not.
| Capability | Sink methods |
|---|---|
| Stdio | print, println, eprintln |
| Net / Fs / Db | get, post · write · exec, query |
Warn-then-enforce: a violation is a warning by default, a hard error under @strict_ifc() (which also catches implicit flows). The single auditable bridge is declassify(value, reason: "…"), recorded in the SBOM. Labels cannot be shed by repackaging into aggregates or mutable containers. The analysis is intra-procedural with whole-aggregate granularity.
8Imports
import util # sibling: ./util.capa
import sinks.csv_sink # nested
import util as U # alias
import util (greet as hi, Color) # selective + renameVisibility
Top-level items are private by default. Mark a function, constant, type, trait or capability pub to expose it to importers; anything without pub is callable only from inside the same file. Selective import import foo (a, b as c) brings only the listed pub symbols into scope, the hygienic way to resolve a name collision between two dependencies (e.g. capa_csv and capa_cli both exporting parse).
Module search paths
The loader resolves import x.y in order: importing-file directory, CAPA_PATH entries, ./vendor/, the parent of every path= entry in capa.toml, ./libraries/, then the root-file directory. The importer-relative path always wins. Set CAPA_PATH (separated by ; on Windows, : elsewhere) to add roots. Python interop uses py_import / py_invoke, both gated by Unsafe.
9The main program
The entry point is a function called main that may take one or more capabilities; the runtime instantiates them at boot. If main returns Result<(), E>, an Err causes a non-zero exit code.
fun main(stdio: Stdio, fs: Fs, env: Env)
let argv = env.args()
stdio.println("received ${argv.length()} arguments")10Attributes
| Attribute | Keys | Role |
|---|---|---|
| @security | cve, cwe, severity, fixed_in | Link a function to a known security history. |
| @deprecated | reason, since, use, removed_in | Mark an API as superseded. |
| @audited | date, by, scope, notes | Record a manual security audit. |
| @vex | cve, status, justification, detail | Per-function CycloneDX VEX exploitability claim. |
| @strict_ifc | (none) | Opt this function into fail-closed information-flow checking: a secret-reaches-public-sink flow becomes a hard error instead of a warning, and implicit flows are caught. |
| @constant_time | (none) | Require this function to be constant-time: the analyzer rejects any control-flow decision or index that depends on a @secret value (CWE-208 timing leaks). |
@vex status accepts the CycloneDX vocabulary (not_affected, affected, fixed, in_triage, false_positive, exploitable, resolved, resolved_with_pedigree).
11Compiler CLI
| Flag | Output |
|---|---|
| repl / test | REPL with caps pre-bound · discover and run tests/test_*.capa. |
| --run / --transpile / --watch | Execute · print generated Python · re-run on change. |
| --wasm [--component] | Compile through CIR to WebAssembly; wrap as a Component Model component. |
| --wasi experimental | Emit a component for stock WASI Preview 2 runtimes (for example wasmtime) instead of the dedicated capa:host. Gives mitigation Levels 1 and 2 but not Level 3; dynamic paths need an operator grant --preopen <dir>. See the roadmap. |
| --manifest | Capa-native JSON: per-function caps, attributes, Unsafe crossings, args_flow. |
| --cyclonedx / --spdx | CycloneDX 1.5 / SPDX 2.3 SBOM with per-function capability metadata. |
| --vex / --provenance | CycloneDX VEX per @vex · SLSA L1 provenance bound to the source SHA-256. |
Supply-chain and governance
These operate over a project (they need a capa.toml root) and are byte-reproducible; the signable ones wrap their output in a content_integrity envelope carrying a sha256 digest, and the compiler holds no keys.
| Flag | Output |
|---|---|
| --manifest-digest | The canonical, content-addressable manifest: the same data as --manifest, serialised key-sorted and wrapped with a sha256 content_integrity envelope. Byte-reproducible across runs and machines. |
| --compose-sbom | The composed capability SBOM for the whole product: attribute each function to its owning package, walk the dependency DAG, and roll the capability surface up bottom-up. An unanalyzable subtree composes as a visible authority-UNKNOWN element that dominates the join. |
| --check-capabilities | CI gate. Compose the product SBOM and verify every package against its declared capa.toml [capabilities] ceiling; exit non-zero on a violation, naming the offending capability and the dependency edge. An authority-UNKNOWN subtree fails closed unless allow_unknown = true. |
| --check-policies | CI gate. Compose the product SBOM and verify it against the organization compliance policies in the product-level capa-policy.toml; exit non-zero on any policy failure with a per-violation message. |
| --conformance-report | Emit the signed conformance report: evaluate every declared capa-policy.toml policy over the composed capability graph and record the per-policy pass/fail results in a content_integrity envelope. |
| --capability-diff <old> <new> | Emit a signed authority changelog between two capability artifacts: per exported function and for the product, which capabilities were gained (widening) or lost (narrowing), and any operator-grant / authority-unknown transition. Functions match by stable identity, so a line move produces no entry. Takes no .capa file. |
| --fail-on-widening | CI gate for --capability-diff. Exit non-zero when the changelog contains any widening (authority gained, a guarantee lost, or an operator grant added) or an authority-UNKNOWN transition. A pure narrowing or no-change exits 0. |
Sandbox and resource ceilings
| Flag | Output |
|---|---|
| --allow-host <host>[:get|:post] | With --wasi, an operator-declared Net grant letting the component reach <host> (the Net analogue of --preopen), unblocking a dynamic, argv-derived URL the compiler otherwise rejects fail-closed. Repeatable; :get grants read only, :post write only, no suffix both. Recorded in the SBOM as operator-declared. |
| --wasm-memory-cap <pages> | With --wasm, cap the emitted linear memory at this many 64 KiB pages; the allocator then traps at a deterministic ceiling instead of a host-dependent OOM point. Default 256 pages (16 MiB); 0 skips the cap. |
| --foreign-fuel <N> | With --wasm --run, bound the CPU an untrusted foreign component may burn per call to N fuel units (~1 per Wasm instruction). A spin then traps cleanly instead of hanging the host. Default 1e9; 0 skips the bound. |
| --foreign-memory-cap <MiB> | With --wasm --run, cap the linear memory an untrusted foreign component may grow to, in MiB. A runaway self-allocation is refused instead of OOM-ing the host. Default 256 MiB; 0 skips the bound. |
12Differences from Python
| Capa | Python |
|---|---|
| Capabilities required for I/O | Globals such as print, open |
| Types checked at compile time | Duck typing |
| Exhaustive match checked | Runtime match, no exhaustiveness |
| Mutation only with var / consume | Everything mutable |
| Manifest / SBOM emitted by compiler | Manual via external tools |
13Known limitations
- String literals are single-line (use \n); nested string literals inside interpolation are unsupported.
- No asynchronous I/O operations.
- if/match in block-body lambdas need => before the indented block.
- Multi-line match inside parentheses requires the inline { } form.
For the full roadmap see the roadmap page and TODO.md.