AffineScript uses an ownership system inspired by Rust and linear types from type theory. This ensures memory safety without garbage collection.
- Core Concepts
- Ownership Rules
- Moving Values
- Borrowing
- Lifetimes
- Quantity Annotations
- Patterns
- Common Errors
Every value in AffineScript has exactly one owner - the variable that holds it. When the owner goes out of scope, the value is automatically cleaned up.
fn example() {
let s = String::from("hello") // s owns the string
// s is valid here
} // s goes out of scope, string is dropped
- Each value has exactly one owner
- Owned values must be used exactly once (affine/linear)
- When the owner goes out of scope, the value is dropped
Values marked own must be consumed exactly once:
struct File {
handle: FileHandle
}
fn process(file: own File) {
// file must be consumed in this function
file.close() // Consumes file
}
fn bad_example(file: own File) {
// ERROR: file is never consumed
} // Compile error: unused owned value
fn also_bad(file: own File) {
file.read()
file.read() // ERROR: file already used
}
By default, function parameters take ownership:
fn consume(s: String) { // Takes ownership
println(s)
} // s is dropped here
fn main() {
let s = String::from("hello")
consume(s) // Ownership transferred to consume
// s is no longer valid here
println(s) // ERROR: use of moved value
}
Some types implement Copy and are duplicated instead of moved:
// Primitive types are Copy
let x: Int = 42
let y = x // x is copied, not moved
println(x) // OK: x still valid
// Explicit Copy trait
struct Point: Copy {
x: Float64,
y: Float64
}
let p1 = Point { x: 0.0, y: 0.0 }
let p2 = p1 // Copied
println(p1.x) // OK
When a value is assigned or passed, it moves (unless it's Copy):
let s1 = String::from("hello")
let s2 = s1 // s1 is MOVED to s2
// s1 is now invalid
fn take(s: String) {
// s is owned here
}
let s3 = String::from("world")
take(s3) // s3 is moved into take()
// s3 is now invalid
Moving part of a struct makes the whole struct unusable:
struct Person {
name: String,
age: Int
}
let p = Person { name: "Alice".into(), age: 30 }
let n = p.name // Partial move of 'name'
// p is now partially moved
println(p.age) // ERROR: p is partially moved
println(p.name) // ERROR: p.name was moved
Functions can return ownership:
fn create() -> String {
String::from("created") // Ownership returned to caller
}
fn transform(s: String) -> String {
s.to_uppercase() // Takes ownership, returns new value
}
let s = create() // s owns the string
let s2 = transform(s) // s moved in, s2 owns result
Multiple shared borrows allow reading:
fn calculate_length(s: &String) -> Int {
s.len() // Can read s
} // s is not dropped (we don't own it)
fn main() {
let s = String::from("hello")
let len = calculate_length(&s) // Borrow s
println(s) // OK: s still valid
}
Exactly one mutable borrow for exclusive access:
fn append(s: &mut String) {
s.push_str(" world") // Can modify s
}
fn main() {
let mut s = String::from("hello")
append(&mut s) // Mutable borrow
println(s) // "hello world"
}
-
At any time, you can have either:
- One mutable reference, OR
- Any number of immutable references
-
References must always be valid (no dangling)
let mut s = String::from("hello")
// OK: Multiple shared borrows
let r1 = &s
let r2 = &s
println(r1, r2)
// ERROR: Can't have mutable while shared exist
let r3 = &mut s // ERROR if r1, r2 still in use
// OK after r1, r2 are done
println(r1, r2) // Last use of r1, r2
let r3 = &mut s // OK now
Mutable references can be reborrowed:
fn use_ref(s: &mut String) {
s.push('!')
}
fn main() {
let mut s = String::from("hello")
let r = &mut s
// Reborrow: r is temporarily "frozen"
use_ref(r) // Implicitly reborrows
r.push('?') // r usable again
}
Most lifetimes are inferred:
fn first(s: &String) -> &Char {
&s.chars()[0]
} // Lifetime of return tied to input
When needed, lifetimes are annotated:
fn longest['a](x: &'a String, y: &'a String) -> &'a String {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("hello")
let result;
{
let s2 = String::from("hi")
result = longest(&s1, &s2)
} // s2 dropped here
println(result) // ERROR: result might reference s2
}
struct Reader['a] {
data: &'a [Byte]
}
impl['a] Reader['a] {
fn new(data: &'a [Byte]) -> Reader['a] {
Reader { data }
}
fn read(self: &Self) -> &'a [Byte] {
self.data
}
}
Must be used exactly once:
fn must_close(file: 1 File) {
// Must call exactly one consuming method
file.close()
}
fn bad(file: 1 File) {
// ERROR: Not used
}
fn also_bad(file: 1 File) {
file.read()
file.close() // ERROR: Used twice
}
Can be used at most once:
fn maybe_use(resource: own Resource) {
if condition {
resource.consume()
}
// OK: resource dropped if not consumed
}
Can be used any number of times (for Copy types):
fn many_uses(x: w Int) -> Int {
x + x + x // OK: unrestricted
}
Compile-time only, no runtime representation:
fn phantom[0 T](x: Int) -> Int {
// T exists for type checking but has no runtime cost
x
}
fn process_file(path: &str) -> Result[(), Error] {
let file = File::open(path)?
// file is automatically closed when function returns
// even if there's an error
let data = file.read_all()?
process(data)?
Ok(())
} // file.close() called automatically
fn with_file[T](
path: &str,
f: (file: &mut File) -> T
) -> Result[T, Error] {
let mut file = File::open(path)?
let result = f(&mut file)
file.close()?
Ok(result)
}
// Usage
let content = with_file("data.txt", |f| {
f.read_all()
})?
struct RequestBuilder {
url: Option[String],
method: Method,
headers: Vec[(String, String)]
}
impl RequestBuilder {
fn new() -> own RequestBuilder { ... }
fn url(self: own Self, url: String) -> own Self {
Self { url: Some(url), ..self }
}
fn header(self: own Self, k: String, v: String) -> own Self {
let mut headers = self.headers;
headers.push((k, v));
Self { headers, ..self }
}
fn build(self: own Self) -> Result[Request, Error] {
// Consumes builder, returns Request
}
}
let request = RequestBuilder::new()
.url("https://example.com")
.header("Content-Type", "application/json")
.build()?
let s = String::from("hello")
let s2 = s
println(s) // ERROR: value moved
Fix: Clone if you need both:
let s = String::from("hello")
let s2 = s.clone()
println(s) // OK
let mut v = vec![1, 2, 3]
let first = &v[0]
v.push(4) // ERROR: v is borrowed
println(first)
Fix: Don't overlap borrows:
let mut v = vec![1, 2, 3]
let first = v[0] // Copy the value
v.push(4) // OK
println(first)
fn dangle() -> &String {
let s = String::from("hello")
&s // ERROR: s will be dropped
}
Fix: Return owned value:
fn no_dangle() -> String {
String::from("hello") // Ownership transferred
}
fn unused(x: 1 Resource) {
// ERROR: x must be consumed
}
Fix: Consume or explicitly drop:
fn used(x: 1 Resource) {
x.close() // Consumed
}
fn dropped(x: 1 Resource) {
drop(x) // Explicitly dropped
}
- Types - Type system overview
- Effects - Effect system
- Memory Model - Memory layout details