Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions ATTENUATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@ Here’s a clean way to model the same thing in capc with what you’ve already

1) Make “caps” affine + non-forgeable

Keep using opaque struct as “this is a capability/handle.” That gives you:
Use `capability struct` as “this is a capability/handle.” That gives you:
• cannot copy (your Token test)
• cannot fabricate (opaque means no public fields / no user construction unless you allow Cap{} for opaques — you probably shouldn’t)
• cannot fabricate (capability types are opaque: no public fields / no user construction outside the defining module)

Also: your current is_affine_type doesn’t automatically make sys.* opaque types affine. If RootCap is supposed to be the “source of authority,” it should be affine too (Austral’s root capability is special and is threaded through main). 
So: add it to AFFINE_ROOTS (whatever its fully-qualified name is in your world, e.g. "sys.RootCap" or "rc.RootCap").
Capability types default to affine unless marked `copy` or `linear`.

2) Encode attenuation as “only downward constructors”

Design the stdlib so the only way to get a capability is from a stronger one, and the API only lets you narrow.

A minimal pattern:

opaque struct RootCap
opaque struct Filesystem
opaque struct Dir
opaque struct FileRead
opaque struct FileWrite
capability struct RootCap
capability struct Filesystem
capability struct Dir
capability struct FileRead
capability struct FileWrite

module fs {
// mint broad cap from root (borrow root so you can mint others too)
Expand Down Expand Up @@ -49,7 +48,7 @@ Notes:

For a capability/handle, this is exactly the point:

opaque struct Token
capability struct Token

pub fn main() -> i32 {
let t = Token{}
Expand All @@ -58,7 +57,7 @@ pub fn main() -> i32 {
return 0
}

It’s only “too restrictive” if Token is meant to be data (copyable value). In that case, don’t make it opaque, or give it a non-affine representation (plain struct of copyable fields).
It’s only “too restrictive” if Token is meant to be data (copyable value). In that case, don’t make it capability/opaque, or give it a non-affine representation (plain struct of copyable fields).

4) The tests that actually prove “caps can’t be reused”

Expand Down
54 changes: 45 additions & 9 deletions TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ match flag {

## 6) Capabilities and attenuation

Capabilities live in `sys.*` and are opaque. You can only get them from `RootCap`.
Capabilities live in `sys.*` and are declared with the `capability` keyword (capability types are opaque). You can only get them from `RootCap`.

```cap
module read_config
Expand All @@ -146,28 +146,64 @@ pub fn main(rc: RootCap) -> i32 {

This is attenuation: each step narrows authority. There is no safe API to widen back.

## 7) Opaque, copy, affine, linear
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):

```cap
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.

## 7) 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:

```cap
opaque struct Token // affine by default (move-only)
copy opaque struct RootCap // unrestricted (copyable)
linear opaque struct FileRead // must be consumed
capability struct Token // affine by default (move-only)
copy capability struct RootCap // unrestricted (copyable)
linear capability struct FileRead // must be consumed
```

Kinds:

- **Unrestricted** (copy): can be reused freely.
- **Affine** (default for opaque): move-only, dropping is OK.
- **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`

## 8) Moves and use-after-move

```cap
module moves

opaque struct Token
capability struct Token

pub fn main() -> i32 {
let t = Token{}
Expand All @@ -184,7 +220,7 @@ Affine and linear values cannot be used after move. If you move in one branch, i
```cap
module linear

linear opaque struct Ticket
linear capability struct Ticket

pub fn main() -> i32 {
let t = Ticket{}
Expand All @@ -202,7 +238,7 @@ There is a small borrow feature for read-only access in function parameters and
```cap
module borrow

opaque struct Cap
capability struct Cap

impl Cap {
pub fn ping(self: &Cap) -> i32 { return 1 }
Expand Down
1 change: 1 addition & 0 deletions capc/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub struct StructDecl {
pub is_opaque: bool,
pub is_linear: bool,
pub is_copy: bool,
pub is_capability: bool,
pub doc: Option<String>,
pub span: Span,
}
Expand Down
2 changes: 2 additions & 0 deletions capc/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub enum TokenKind {
Impl,
#[token("opaque")]
Opaque,
#[token("capability")]
Capability,
#[token("copy")]
Copy,
#[token("linear")]
Expand Down
36 changes: 26 additions & 10 deletions capc/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ impl Parser {
let mut is_linear = false;
let mut is_copy = false;
let mut is_opaque = false;
let mut is_capability = false;
loop {
match self.peek_kind() {
Some(TokenKind::Pub) => {
Expand Down Expand Up @@ -155,6 +156,15 @@ impl Parser {
self.bump();
is_opaque = true;
}
Some(TokenKind::Capability) => {
if is_capability {
return Err(self.error_current(
"duplicate `capability` modifier".to_string(),
));
}
self.bump();
is_capability = true;
}
_ => break,
}
}
Expand All @@ -164,18 +174,19 @@ impl Parser {
));
}
if self.peek_kind() == Some(TokenKind::Extern) {
if is_opaque || is_linear || is_copy {
if is_opaque || is_linear || is_copy || is_capability {
return Err(self.error_current(
"linear/copy/opaque applies only to struct declarations".to_string(),
"linear/copy/opaque/capability applies only to struct declarations".to_string(),
));
}
return Ok(Item::ExternFunction(self.parse_extern_function(is_pub, doc)?));
}
match self.peek_kind() {
Some(TokenKind::Fn) => {
if is_opaque || is_linear || is_copy {
if is_opaque || is_linear || is_copy || is_capability {
return Err(self.error_current(
"linear/copy/opaque applies only to struct declarations".to_string(),
"linear/copy/opaque/capability applies only to struct declarations"
.to_string(),
));
}
Ok(Item::Function(self.parse_function(is_pub, doc)?))
Expand All @@ -185,12 +196,14 @@ impl Parser {
is_opaque,
is_linear,
is_copy,
is_capability,
doc,
)?)),
Some(TokenKind::Enum) => {
if is_opaque || is_linear || is_copy {
if is_opaque || is_linear || is_copy || is_capability {
return Err(self.error_current(
"linear/copy/opaque applies only to struct declarations".to_string(),
"linear/copy/opaque/capability applies only to struct declarations"
.to_string(),
));
}
Ok(Item::Enum(self.parse_enum(is_pub, doc)?))
Expand All @@ -201,9 +214,10 @@ impl Parser {
"impl blocks cannot be marked pub".to_string(),
));
}
if is_opaque || is_linear || is_copy {
if is_opaque || is_linear || is_copy || is_capability {
return Err(self.error_current(
"linear/copy/opaque applies only to struct declarations".to_string(),
"linear/copy/opaque/capability applies only to struct declarations"
.to_string(),
));
}
Ok(Item::Impl(self.parse_impl_block(doc)?))
Expand Down Expand Up @@ -330,6 +344,7 @@ impl Parser {
is_opaque: bool,
is_linear: bool,
is_copy: bool,
is_capability: bool,
doc: Option<String>,
) -> Result<StructDecl, ParseError> {
let start = self.expect(TokenKind::Struct)?.span.start;
Expand All @@ -355,10 +370,10 @@ impl Parser {
}
}
let end = self.expect(TokenKind::RBrace)?.span.end;
if is_opaque && !fields.is_empty() {
if (is_opaque || is_capability) && !fields.is_empty() {
return Err(self.error_at(
Span::new(start, end),
"opaque struct cannot declare fields".to_string(),
"opaque/capability struct cannot declare fields".to_string(),
));
}
end
Expand All @@ -372,6 +387,7 @@ impl Parser {
is_opaque,
is_linear,
is_copy,
is_capability,
doc,
span: Span::new(start, end),
})
Expand Down
4 changes: 2 additions & 2 deletions capc/src/typeck/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1594,7 +1594,7 @@ pub(super) fn check_expr(
if info.is_opaque && info.module != module_name {
return Err(TypeError::new(
format!(
"cannot access fields of opaque type `{struct_name}` outside module `{}`",
"cannot access fields of opaque/capability type `{struct_name}` outside module `{}`",
info.module
),
field_access.span,
Expand Down Expand Up @@ -1976,7 +1976,7 @@ fn check_struct_literal(
if info.is_opaque && info.module != module_name {
return Err(TypeError::new(
format!(
"cannot construct opaque type `{}` outside module `{}`",
"cannot construct opaque/capability type `{}` outside module `{}`",
key, info.module
),
lit.span,
Expand Down
7 changes: 5 additions & 2 deletions capc/src/typeck/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub(super) fn collect_functions(
entry_name: &str,
stdlib: &StdlibIndex,
struct_map: &HashMap<String, StructInfo>,
enum_map: &HashMap<String, EnumInfo>,
) -> Result<HashMap<String, FunctionSig>, TypeError> {
let mut functions = HashMap::new();
for module in modules {
Expand Down Expand Up @@ -118,6 +119,7 @@ pub(super) fn collect_functions(
&local_use,
stdlib,
struct_map,
enum_map,
)?;
for method in methods {
if let Some((impl_ty, method_name)) = method.name.item.split_once("__") {
Expand Down Expand Up @@ -195,14 +197,15 @@ pub(super) fn collect_structs(
TypeKind::Linear
} else if decl.is_copy {
TypeKind::Unrestricted
} else if decl.is_opaque {
} else if decl.is_opaque || decl.is_capability {
TypeKind::Affine
} else {
TypeKind::Unrestricted
};
let info = StructInfo {
fields,
is_opaque: decl.is_opaque,
is_opaque: decl.is_opaque || decl.is_capability,
is_capability: decl.is_capability,
kind: declared_kind,
module: module_name.clone(),
};
Expand Down
6 changes: 4 additions & 2 deletions capc/src/typeck/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ pub(super) fn lower_module(
use_map,
stdlib,
structs,
enums,
)?;
for method in methods {
let hir_func = lower_function(&method, &mut ctx)?;
Expand Down Expand Up @@ -175,7 +176,7 @@ pub(super) fn lower_module(
hir_structs.push(HirStruct {
name: decl.name.item.clone(),
fields: fields?,
is_opaque: decl.is_opaque,
is_opaque: decl.is_opaque || decl.is_capability,
});
}
}
Expand Down Expand Up @@ -826,7 +827,7 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result<HirExpr
if info.is_opaque && info.module != ctx.module_name {
return Err(TypeError::new(
format!(
"cannot construct opaque type `{}` outside module `{}`",
"cannot construct opaque/capability type `{}` outside module `{}`",
key, info.module
),
lit.span,
Expand Down Expand Up @@ -1042,6 +1043,7 @@ mod tests {
StructInfo {
fields: HashMap::new(),
is_opaque: false,
is_capability: false,
kind: TypeKind::Unrestricted,
module: "foo".to_string(),
},
Expand Down
Loading
Loading