+
Capable in 15 Minutes
+
Capable is a small capability-secure systems language. The main idea: authority is a value. If you didn't receive a capability, you can't do the thing.
+
This tutorial is a quick tour of the current language slice and the capability model.
+
1) Hello, console
+
module hello
+use sys::system
+
+pub fn main(rc: RootCap) -> i32 {
+ let c = rc.mint_console()
+ c.println("hello")
+ return 0
+}
+
RootCap is the root authority passed to main. It can mint narrower capabilities (console, filesystem, etc.).
+
2) Basic syntax
+
module basics
+
+pub fn add(a: i32, b: i32) -> i32 {
+ return a + b
+}
+
+pub fn main() -> i32 {
+ let x = 1
+ let y: i32 = 2
+ if (x < y) {
+ return add(x, y)
+ } else {
+ return 0
+ }
+}
+
+- Statements:
let, assignment, if, while, for, return, match, defer.
+- Expressions: literals, calls, binary ops, unary ops, method calls.
+- Modules + imports:
module ... and use ... (aliases by last path segment).
+- If a function returns
unit, you can omit the -> unit annotation.
+for { ... } is an infinite loop (Go style); for i in a..b is range.
+- Integer arithmetic traps on overflow.
+- Variable shadowing is not allowed.
+
+
3) Structs and enums
+
module types
+
+struct Pair { left: i32, right: i32 }
+
+enum Color { Red, Green, Blue }
+
Structs and enums are nominal types. Enums are currently unit variants only.
+
4) Defer
+
defer schedules a function or method call to run when the current scope
+
exits (LIFO order). Arguments are evaluated at the defer site.
+
pub fn main(rc: RootCap) -> i32 {
+ let c = rc.mint_console()
+ c.println("start")
+ defer c.println("cleanup")
+ c.println("end")
+ return 0
+}
+
Current restriction: the deferred expression must be a call.
+
5) Methods
+
Methods are defined in impl blocks and lower to Type__method at compile time.
+
module methods
+
+struct Pair { left: i32, right: i32 }
+
+impl Pair {
+ pub fn sum(self) -> i32 { return self.left + self.right }
+ pub fn add(self, x: i32) -> i32 { return self.sum() + x }
+ pub fn peek(self: &Pair) -> i32 { return self.left }
+}
+
Method receivers can be self (move) or self: &T (borrow‑lite, read‑only).
+
6) Results, match, and ?
+
module results
+
+pub fn main() -> i32 {
+ let ok: Result[i32, i32] = Ok(10)
+ match ok {
+ Ok(x) => { return x }
+ Err(e) => { return 0 }
+ }
+}
+
Result[T, E] is the only generic type today and is special-cased by the compiler.
+
Inside a function that returns Result, you can use ? to unwrap or return early:
+
module results_try
+
+fn read_value() -> Result[i32, i32] {
+ return Ok(7)
+}
+
+fn use_value() -> Result[i32, i32] {
+ let v = read_value()?
+ return Ok(v + 1)
+}
+
You can also unwrap with defaults:
+
let v = make().unwrap_or(0)
+let e = make().unwrap_err_or(0)
+
Matches must be exhaustive; use _ to cover the rest:
+
match flag {
+ true => { }
+ false => { }
+}
+
You can also use if let as a single-arm match:
+
if let Ok(x) = make() {
+ return x
+} else {
+ return 0
+}
+
7) Capabilities and attenuation
+
Capabilities live in sys.* and are declared with the capability keyword (capability types are opaque). You can only get them from RootCap.
+
module read_config
+use sys::system
+use sys::fs
+
+pub fn main(rc: RootCap) -> i32 {
+ let fs = rc.mint_filesystem("./config")
+ let dir = fs.root_dir()
+ let file = dir.open_read("app.txt")
+
+ match file.read_to_string() {
+ Ok(s) => { rc.mint_console().println(s); return 0 }
+ Err(e) => { return 1 }
+ }
+}
+
This is attenuation: each step narrows authority. There is no safe API to widen back.
+
To make attenuation one-way at compile time, any method that returns a capability must take self by value. Methods that take &self cannot return capabilities.
+
Example of what is rejected (and why):
+
capability struct Dir
+capability struct FileRead
+
+impl Dir {
+ pub fn open(self: &Dir, name: string) -> FileRead {
+ let file = self.open_read(name)
+ return file
+ }
+}
+
Why this is rejected:
+
+Dir can read many files (more power).
+FileRead can read one file (less power).
+- The bad example lets you keep the more powerful
Dir and also get a FileRead.
+- We want “one-way” attenuation: when you make something less powerful, you give up the more powerful one.
+
+
So methods that return capabilities must take self by value, which consumes the old capability.
+
8) Capability, opaque, copy, affine, linear
+
capability struct is the explicit “this is an authority token” marker. Capability types are always opaque (no public fields, no user construction) and default to affine unless marked copy or linear. This exists so the capability surface is obvious in code and the compiler can enforce one‑way attenuation (methods returning capabilities must take self by value).
+
Structs can declare their kind:
+
capability struct Token
+copy capability struct RootCap
+linear capability struct FileRead
+
Kinds:
+
+- Unrestricted (copy): can be reused freely.
+- Affine (default for capability/opaque): move-only, dropping is OK.
+- Linear: move-only and must be consumed on all paths.
+
+
Use capability struct for authority-bearing tokens. Use opaque struct for unforgeable data types that aren’t capabilities.
+
In the current stdlib:
+
+copy capability: RootCap, Console, Args
+copy opaque: Alloc, Buffer, Slice, MutSlice, VecU8, VecI32, VecString
+capability (affine): ReadFS, Filesystem, Dir, Stdin
+linear capability: FileRead
+
+
9) Moves and use-after-move
+
module moves
+
+capability struct Token
+
+pub fn main() -> i32 {
+ let t = Token{}
+ let u = t
+ let v = t
+ return 0
+}
+
Affine and linear values cannot be used after move. If you move in one branch, it's moved after the join.
+
10) Linear must be consumed
+
module linear
+
+linear capability struct Ticket
+
+pub fn main() -> i32 {
+ let t = Ticket{}
+ drop(t)
+ return 0
+}
+
Linear values must be consumed along every path. You can consume them with a terminal method (like FileRead.close() or read_to_string()), or with drop(x) as a last resort.
+
11) Borrow-lite: &T parameters
+
There is a small borrow feature for read-only access in function parameters and locals.
+
module borrow
+
+capability struct Cap
+
+impl Cap {
+ pub fn ping(self: &Cap) -> i32 { return 1 }
+}
+
+pub fn twice(c: &Cap) -> i32 {
+ let a = c.ping()
+ let b = c.ping()
+ return a + b
+}
+
Rules:
+
+&T is allowed on parameters and locals.
+- Reference locals must be initialized from another local value.
+- References cannot be stored in structs, enums, or returned.
+- References are read-only: they can only satisfy
&T parameters.
+- Passing a value to
&T implicitly borrows it.
+
+
This avoids a full borrow checker while making non-consuming observers ergonomic.
+
12) Safety boundary
+
package safe is default. Raw pointers and extern calls require package unsafe.
+
package unsafe
+module ffi
+
+extern fn some_ffi(x: i32) -> i32
+
13) Raw pointers and unsafe
+
Raw pointers are available as *T, but only in package unsafe.
+
package unsafe
+module pointers
+
+pub fn main(rc: RootCap) -> i32 {
+ let alloc = rc.mint_alloc_default()
+ let ptr: *u8 = alloc.malloc(16)
+ alloc.free(ptr)
+ return 0
+}
+
There is no borrow checker for pointers. Use them only inside package unsafe.
+
14) What exists today (quick list)
+
+- Methods, modules, enums, match, while, if
+- Opaque capability handles in
sys.*
+- Linear/affine checking with control-flow joins
+- Borrow-lite
&T parameters
+
+
+
That should be enough to read and write small Capable programs, and understand how attenuation and linearity fit together.
+