Borrow Checker
Ferret’s borrow checker is a compile-time analysis system that ensures memory safety without garbage collection. It enforces strict rules about how references can be used, preventing common bugs like data races, use-after-free, and iterator invalidation.
Core Concepts
Section titled “Core Concepts”Ownership and Copies
Section titled “Ownership and Copies”Every binding owns its value. Ferret copies by default, so assigning or returning a value creates a copy unless you explicitly move it. Literals bind directly into the destination without an extra copy.
fn main() { let x := 42; let y := x; // copy let z := @x; // move; x is no longer usable}Moves (@)
Section titled “Moves (@)”Use @ to move a value out of a binding. Moves are only allowed on identifiers (phase 1), cannot move references, and cannot happen while the value is borrowed.
let a := 10;let b := @a; // a moved// let c := a; // ❌ Error: use of moved valueBorrowing
Section titled “Borrowing”Instead of copying or moving, you can borrow a value by taking a reference to it:
let arr := [1, 2, 3, 4, 5];let arr_ref := &arr; // Borrow arr immutablyBorrowing allows multiple parts of your code to access the same data without copying.
The Borrow Rules
Section titled “The Borrow Rules”Ferret enforces three fundamental rules at compile time:
Rule 1: Multiple Immutable Borrows
Section titled “Rule 1: Multiple Immutable Borrows”You can have as many immutable references (&T) as you want:
let data := [10, 20, 30];
let ref1 := &data;let ref2 := &data;let ref3 := &data;
// ✅ All valid - multiple readers allowedio::Println(*ref1);io::Println(*ref2);io::Println(*ref3);Why? Reading data is always safe - multiple readers cannot interfere with each other.
Rule 2: Only One Mutable Borrow
Section titled “Rule 2: Only One Mutable Borrow”You can have only ONE mutable reference (&mut T) at a time:
let arr := [1, 2, 3];
let mut_ref1 := &mut arr;let mut_ref2 := &mut arr; // ❌ ERROR: cannot borrow arr as mutable more than once
*mut_ref1 = 10; // Would cause issues if mut_ref2 existedWhy? Multiple writers could cause data races - only one writer ensures safety.
Rule 3: No Mixed Borrows
Section titled “Rule 3: No Mixed Borrows”You cannot have mutable and immutable borrows at the same time:
let data := [1, 2, 3];
let immut_ref := &data;let mut_ref := &mut data; // ❌ ERROR: cannot borrow data as mutable while immutably borrowed
io::Println(*immut_ref); // Reader would see unexpected changes*mut_ref = 99; // Writer would invalidate readerWhy? Readers expect data to stay unchanged, but writers modify it - mixing them breaks that expectation.
Borrow Scope
Section titled “Borrow Scope”Borrows have a scope - they’re only active while they’re being used:
let arr := [10, 20, 30];
{ let ref1 := &arr; io::Println(*ref1); // ref1 used here} // ref1 goes out of scope - borrow ends
// ✅ Can borrow again after previous borrow endedlet ref2 := &arr;Early Release
Section titled “Early Release”Borrows are released when they’re no longer needed:
let arr := [1, 2, 3];
let immut_ref := &arr;io::Println(*immut_ref); // Last use of immut_ref// Borrow ends after last use
// ✅ Can now take mutable borrowlet mut_ref := &mut arr;*mut_ref = 10;Common Patterns
Section titled “Common Patterns”Passing References to Functions
Section titled “Passing References to Functions”Function calls automatically release borrows after the call:
fn process_data(data: &[]i32) { // Use data...}
let arr := [1, 2, 3];
process_data(&arr); // Borrow during callprocess_data(&arr); // ✅ Can borrow again - previous borrow releasedMethod Receivers
Section titled “Method Receivers”Methods automatically borrow self with the appropriate type:
type Counter struct { .value: i32,};
fn (c: &mut Counter) increment() { c.value++; // Auto-borrows as &mut}
fn (c: &Counter) get() -> i32 { return c.value; // Auto-borrows as &}
let counter := { .value: 0 } as Counter;
counter.increment(); // Borrows as &mutcounter.increment(); // ✅ Previous borrow releasedlet val := counter.get(); // Borrows as &Container Mutations
Section titled “Container Mutations”Built-in functions follow borrow rules:
let arr := [1, 2, 3];
// ✅ Multiple immutable useslet len1 := len(&arr);let len2 := len(&arr);
// ✅ Mutable operationsappend(&mut arr, 4);append(&mut arr, 5);
// ❌ ERROR: Can't mix mutable and immutablelet len := len(&arr);append(&mut arr, 6); // ERROR if len borrow still activeDisjoint Borrows
Section titled “Disjoint Borrows”Ferret allows borrowing different fields of a struct simultaneously:
type Point struct { .x: i32, .y: i32,};
let p := { .x: 10, .y: 20 } as Point;
let x_ref := &p.x;let y_mut := &mut p.y; // ✅ OK - different fields
io::Println(*x_ref); // Read x*y_mut = 30; // Modify yWhy it works: x and y are distinct memory locations - modifying one doesn’t affect the other.
Conservative array indexing: For arrays, the borrow checker is conservative:
let arr := [1, 2, 3];
let elem0 := &arr[0];let elem1 := &mut arr[1]; // ❌ ERROR: assumes all indices might overlap
// Borrow checker can't prove arr[0] and arr[1] don't overlapThis is safe but conservative - it prevents bugs even if your indices don’t actually overlap.
Lifetime Basics
Section titled “Lifetime Basics”Every reference has a lifetime - the scope where it’s valid:
let r: &i32; // Declared but not initialized
{ let x := 42; r = &x; // ❌ ERROR: x doesn't live long enough} // x destroyed here, but r would still reference it
// r would be a dangling reference hereThe rule: A reference cannot outlive the value it points to.
Function Lifetimes
Section titled “Function Lifetimes”When returning references, the borrow checker ensures safety:
fn get_first(arr: &[]i32) -> &i32 { return &arr[0]; // ✅ OK - returned reference lives as long as input}
let data := [10, 20, 30];let first := get_first(&data); // first borrows from dataio::Println(*first);Borrow Checker Error Messages
Section titled “Borrow Checker Error Messages”The compiler provides detailed error messages:
let arr := [1, 2, 3];let r1 := &arr;let r2 := &mut arr; // ERRORerror[B0002]: cannot borrow 'arr' as mutable because it is also borrowed as immutable --> example.fer:3:11 |2 | let r1 := &arr; | ---- immutable borrow occurs here3 | let r2 := &mut arr; | ~~~~~~~~ mutable borrow occurs here4 | io::Println(*r1); | --- immutable borrow later used hereUsing &mut in Read Contexts
Section titled “Using &mut in Read Contexts”You can pass a mutable reference to read-only operations without creating a second borrow:
fn process(data: &mut []i32) { // data is &mut []i32 let len := len(data); // Read through &mut append(data, 100); // Continue using the same &mut}The borrow checker treats this as a read through the same mutable reference.
Design Rationale
Section titled “Design Rationale”Why Borrow Checking?
Section titled “Why Borrow Checking?”Without borrow checking:
// C/C++ style - can cause crasheslet arr := [1, 2, 3];let ptr1 := &arr[0];arr.clear(); // Destroys arrayprintln(*ptr1); // ⚠️ Use-after-free bug!With borrow checking:
let arr := [1, 2, 3];let ref := &arr[0];arr = []; // ❌ ERROR: cannot modify arr while borrowedio::Println(*ref); // Safe - compiler prevents the bugPerformance Benefits
Section titled “Performance Benefits”- Zero runtime cost: All checks happen at compile time
- No garbage collector: Deterministic cleanup
- No reference counting: Direct memory access
- Cache friendly: Predictable memory layout
Comparison with Other Languages
Section titled “Comparison with Other Languages”| Language | Memory Safety | Runtime Cost | Learning Curve |
|---|---|---|---|
| C/C++ | Manual (unsafe) | Zero | High (manual) |
| Java/C# | Garbage Collector | Medium-High | Low |
| Rust | Borrow Checker | Zero | High |
| Go | Garbage Collector | Low-Medium | Low |
| Ferret | Borrow Checker | Zero | Medium |
Ferret aims for Rust’s safety with a gentler learning curve.
Common Mistakes
Section titled “Common Mistakes”Mistake 1: Holding Borrows Too Long
Section titled “Mistake 1: Holding Borrows Too Long”let arr := [1, 2, 3];
let ref := &arr;// ... lots of code ...append(&mut arr, 4); // ❌ ERROR: still borrowed by refSolution: Limit borrow scope:
let arr := [1, 2, 3];
{ let ref := &arr; // Use ref here} // Borrow ends
append(&mut arr, 4); // ✅ OKMistake 2: Returning Local References
Section titled “Mistake 2: Returning Local References”fn create_ref() -> &i32 { let x := 42; return &x; // ❌ ERROR: x doesn't live long enough}Solution: Return the value, not a reference:
Use return @x; when you want to move a value instead of copying it.
fn create_value() -> i32 { let x := 42; return x; // ✅ OK - returns a copy}Mistake 3: Modifying While Iterating
Section titled “Mistake 3: Modifying While Iterating”let arr := [1, 2, 3];
for elem in &arr { // Borrows arr immutably append(&mut arr, 10); // ❌ ERROR: can't mutably borrow while iterating}Solution: Collect changes, then apply:
let arr := [1, 2, 3];let to_add := [];
for elem in &arr { append(&mut to_add, 10);}
for item in &to_add { append(&mut arr, *item); // ✅ OK}Best Practices
Section titled “Best Practices”1. Prefer Short-Lived Borrows
Section titled “1. Prefer Short-Lived Borrows”// ❌ Bad - borrow lives too longlet ref := &data;// ... 100 lines of code ...process(ref);
// ✅ Good - borrow only when neededprocess(&data);2. Use Scopes to Control Lifetimes
Section titled “2. Use Scopes to Control Lifetimes”let arr := [1, 2, 3];
{ let temp_ref := &arr; // Work with temp_ref} // Borrow ends here
// Now arr is available for mutable access3. Pass by Reference for Large Types
Section titled “3. Pass by Reference for Large Types”type HugeData struct { .buffer: [10000]i32, // ... more fields};
// ✅ Good - avoids copyfn process(data: &HugeData) { // ...}
// ❌ Bad - expensive copyfn process(data: HugeData) { // ...}4. Use Immutable Borrows by Default
Section titled “4. Use Immutable Borrows by Default”Start with &T and only use &mut T when you need to modify:
// ✅ Good - immutable by defaultfn calculate(data: &[]i32) -> i32 { // Read-only access}
// Only use &mut when necessaryfn sort(data: &mut []i32) { // Modifies the array}Next Steps
Section titled “Next Steps”- Reference Types - Learn reference syntax and usage
- Methods - Use references in method receivers
- Error Handling - Borrow checking with Result types
- Practical Examples - Real-world borrow patterns