References and Ownership

Memory safety without garbage collection. That's the promise of ownership tracking: the compiler helps you avoid use-after-free, double-free, and data races, all at compile time with zero runtime cost.

Ownership Fundamentals

Every value has an owner. Simple rules keep things safe:

  • Every value has exactly one owner
  • When the owner goes out of scope, the value is dropped
  • You can borrow values temporarily (immutably or mutably)
  • The compiler ensures borrows don't outlive the owned value
  • Mutable borrows are exclusive — no other borrows can exist

Example:

type Buffer {
    data [u8; 1024]
}

$buf = Buffer { data: [0; 1024] };  // $buf owns the buffer

{
    $r = &$buf;  // $r borrows $buf
    echo $r.data[0];
}  // $r goes out of scope, borrow ends

// $buf is still valid here

Parameter Passing: Smart Defaults

Functions need to receive data. In Coco, the compiler automatically chooses the most efficient passing strategy based on the type, while still tracking ownership for safety.

The Rules

Small types (≤16 bytes of copyable data):

  • Passed by value (copied)
  • Cheap to copy, no overhead

Large types (structs, arrays, strings):

  • Passed by reference automatically
  • Ownership tracked by compiler
  • No syntax required — it just works

Small Types: Pass by Value

fn increment($x i32): i32 {
    return $x + 1;  // $x is a copy, original unchanged
}

$a = 5;
$b = increment($a);
// $a is still 5, $b is 6

Primitives (i32, f64, bool, etc.) and small structs are copied when passed. This is efficient — just a register move or small stack copy.

Large Types: Pass by Reference

type BigData {
    buffer [u8; 4096]
    count usize
}

fn process($data BigData) {
    // $data is actually a reference to the original
    // No 4KB copy happening here!
    echo $data.count;
}

$big = BigData { buffer: [0; 4096], count: 100 };
process($big); // Efficient: just passes a reference

The compiler sees BigData is large (4KB+), so it automatically passes a reference. The ownership system ensures the reference is valid — no use-after-free possible.

Strings: Always Passed Efficiently

Strings use ARC (automatic reference counting), so they're always passed by reference internally:

fn print_message($msg string) {
    echo $msg; // $msg is a reference-counted pointer
}

$text = "Hello, World!";
print_message($text); // Cheap: increment ref count, pass pointer

No 13-byte copy, no heap allocation — just a ref count bump and a pointer pass.

You Control Ownership

If you want to explicitly transfer ownership, use move semantics:

fn take_ownership($data BigData) {
    // Caller can no longer use $data
}

fn borrow($data &BigData) {
    // Caller retains ownership
}

Wait, but didn't we say large types pass by reference automatically? Yes! But there's a difference:

  • Implicit reference: fn process($data BigData) — compiler passes by reference for efficiency, ownership rules still apply
  • Explicit borrow: fn borrow($data &BigData) — explicitly says "I'm just looking, caller keeps ownership"

The explicit & is about ownership semantics, not performance. Use it when you want to be explicit about borrowing.

Performance Table

Type Size Passing Strategy Cost
i32 4 bytes By value (copy) ~1 instruction
f64 8 bytes By value (copy) ~1 instruction
(i32, i32) 8 bytes By value (copy) ~1 instruction
Small struct ≤16 bytes By value (copy) Few instructions
string 8 bytes By reference (ARC) Ref count + pointer
Large struct >16 bytes By reference Pointer pass
Array [T; N] Varies By reference if large Pointer pass

Why This Design?

No extra syntax: You write $x i32 and $data BigData the same way. The compiler optimizes.

Still safe: The borrow checker tracks references. No use-after-free, no data races.

Predictable: The rules are simple. Small = copy, large = reference. You can reason about performance.

Transparent: It's documented. You know what's happening.

Why This Works

Ownership eliminates whole classes of bugs at compile time:

  • No use-after-free: Can't use a value after it's been freed
  • No double-free: Value is freed exactly once when owner goes out of scope
  • No data races: Mutable borrows are exclusive — only one mutable reference at a time
  • No iterator invalidation: Can't modify a collection while iterating over it

All checked at compile time. Zero runtime cost.

Borrowing: Simple References

Coco uses just & for references. The compiler detects whether you mutate through a reference and enforces safety automatically.

$x = 42;
$x = 100; // Variables are mutable by default

Multiple Readers

When you only read through references, multiple borrows can coexist:

$x = 42;
$r1 = &$x;
$r2 = &$x;
$r3 = &$x;

// All valid — reading doesn't interfere
echo *$r1;
echo *$r2;
echo *$r3;

The compiler sees you're only reading, so multiple borrows are fine.

Exclusive Writing

When you mutate through a reference, the compiler enforces exclusive access:

$x = 42;
$r = &$x;

*$r = 100; // Compiler detects mutation

// Can't create another reference to $x while $r is used for writing
// This ensures no aliasing during mutation

The compiler's flow analysis detects that you're modifying through $r, so it ensures no other borrows can be active during that mutation. This prevents data races at compile time.

How the Compiler Knows

The compiler tracks how you use each reference:

fn just_reading($data &BigStruct) {
    echo $data.field; // Only reading: multiple borrows OK
}

fn mutating($data &BigStruct) {
    $data.field = 10; // Mutation detected: exclusive access required
}

You write the same &BigStruct in both cases. The compiler analyzes the function body and enforces the appropriate rules.

No &mut ceremony — just & everywhere. The compiler handles the rest.

Strings and Ownership

Strings are special — they use automatic reference counting (ARC) instead of pure move semantics.

How String Ownership Works

When you copy a string, you don't copy the data — you increment a reference count:

$s1 = "hello";
$s2 = $s1; // Ref count: 2, both point to same data

// Both $s1 and $s2 are valid, both "own" the string

This is different from move semantics, where ownership transfers:

type Buffer {
    data [u8; 1024]
}

$buf1 = Buffer { data: [0; 1024] };
$buf2 = $buf1; // Ownership moves to $buf2

// $buf1 is no longer valid! (move semantics)

String Borrowing Still Works

Even though strings use ARC, you can still borrow them:

fn read_string($s &string) {
    echo $s; // Just reading
}

fn modify_string($s &string) {
    $s.push_str(" world"); // Compiler detects mutation
}

$text = "hello";
read_string(&$text);   // Reading
modify_string(&$text); // Mutating (compiler enforces exclusive access)

Copy-on-Write Protects Shared Data

If multiple string variables share the same data, and you modify one, Coco makes a copy first:

$s1 = "hello";
$s2 = $s1; // Both share the same data

$s2.push_str(" world"); // Copy-on-write: $s2 gets its own copy

// $s1 = "hello"
// $s2 = "hello world"

This ensures safety: you can't accidentally modify a string that other code is using.

Why ARC for Strings?

ARC makes strings simple to use:

  • No need to think about ownership transfer
  • Cheap to pass around (just ref count ops)
  • Still safe (copy-on-write prevents shared mutation)

The trade-off:

  • Slight overhead (ref count increment/decrement)
  • Not zero-cost like pure move semantics

It's a deliberate choice: ergonomics and simplicity over absolute zero-cost.

The Borrow Checker

The compiler enforces these rules automatically. It tracks:

  • Which values are owned where
  • Which values are borrowed and for how long
  • Whether you mutate through a reference
  • Whether borrows conflict (mutation requires exclusive access)

If the rules are violated, you get a compile error. No runtime checks, no performance cost.

Example of a borrow error:

$x = 42;
$r1 = &$x;
$r2 = &$x;

echo *$r1;  // Reading: fine
*$r2 = 100; // ERROR: can't mutate while other borrows exist
            // Multiple borrows exist, but you tried to write through one

Trusting the Programmer

The borrow checker is a guide, not a prison. It helps you write safe code by default.

But we trust programmers to know what they're doing. When you need manual control:

  • Use raw pointers (*T) for manual memory management
  • Perform any operation you need in your code
  • The type system helps where it can, then gets out of your way

There are no "unsafe" blocks because there's no artificial boundary between "safe" and "unsafe" code. You use the type system's guarantees when they help, and you work around them when you need to.

The language provides powerful tools — ownership, borrowing, the borrow checker — but doesn't force them on you when they don't fit your needs.

Null Safety

No null pointers in regular references. If something might be absent, use Option<T>:

enum Option<T> {
    Some(T),
    None
}

Example:

$maybe = Some(42);

match $maybe {
    Some($value) => echo "Got: {$value}";,
    None => echo "Nothing here";
}

You must explicitly handle the None case. The compiler ensures you don't forget.

Raw pointers can be null, but you're explicitly working with pointers at that point — you know what you're doing.

Type Safety

The type system prevents common mistakes:

  • No implicit lossy conversions — no surprise truncation
  • Explicit casts when you want them$x as u8
  • Exhaustive pattern matching on enums — handle all cases
  • Type system prevents misuse — can't pass the wrong type

Example:

enum Status {
    Ok,
    Error,
    Pending
}

$s = Status::Ok;

match $s {
    Status::Ok => { / handle ok / },
    Status::Error => { / handle error / },
    Status::Pending => { / handle pending / }
    // Compiler ensures all cases are covered
}

The Power of Simplicity

Simple rules, powerful results:

  • Every value has an owner
  • Borrow with & — compiler detects mutation
  • Multiple readers OR one writer (enforced automatically)
  • Use raw pointers when you need manual control

That's it. The type system catches bugs, and you write correct code without fighting the compiler.

No let, no mut, no &mut — just simple, safe code.

Copyright (c) 2025 Ocean Softworks, Sharkk