Skip to main content

Ownership and Borrowing

Oxide uses idiomatic borrowing syntax inspired by Swift and Mojo, making Rust's ownership system more intuitive.

Parameter Ownership

Oxide simplifies parameter ownership with clear keywords:

MeaningOxide SyntaxRust EquivalentCall Site
Read-only (default)x: Tx: &Tf(x) - auto &
Mutable borrowinout x: Tx: &mut Tf(&x) - explicit &
Take ownershipconsuming x: Tx: Tf(x) - no change

Default: Immutable Borrow

By default, parameters are borrowed immutably. The & is added automatically:

fn greet(name: String) {
println!("Hello, $name!")
}

fn main() {
let name = "World"
greet(name) // No & needed - auto borrowed
greet(name) // Can use again - only borrowed
}

Transpiles to:

fn greet(name: &String) {
println!("Hello, {}!", name);
}

fn main() {
let name = "World".to_string();
greet(&name); // & added by transpiler
greet(&name);
}

Mutable Borrow: inout

Use inout when you need to modify a value. The caller must use &:

fn double(inout x: Int) {
x *= 2 // No *x needed - implicit dereference
}

fn appendItem(inout list: Vec<Int>, item: Int) {
list.push(item)
}

fn main() {
var n = 21
double(&n) // & required for visibility
println!("$n") // 42

var items = [1, 2, 3]
appendItem(&items, 4)
}

Transpiles to:

fn double(x: &mut isize) {
*x *= 2;
}

fn append_item(list: &mut Vec<isize>, item: isize) {
list.push(item);
}

fn main() {
let mut n = 21;
double(&mut n);
println!("{}", n);

let mut items = vec![1, 2, 3];
append_item(&mut items, 4);
}

The & at the call site makes mutations visible to readers.

Taking Ownership: consuming

Use consuming when the function takes ownership of the value:

fn consume(consuming s: String): Int {
s.len() // s is owned, can be moved
}

fn takeOver(consuming items: Vec<Int>) {
for item in items {
process(item)
}
}

fn main() {
let s = "hello"
let len = consume(s) // s is moved
// println!(s) // Error: s was moved

let data = [1, 2, 3]
takeOver(data) // data is moved
}

Transpiles to:

fn consume(s: String) -> isize {
s.len() as isize
}

fn take_over(items: Vec<isize>) {
for item in items {
process(item);
}
}

fn main() {
let s = "hello".to_string();
let len = consume(s);

let data = vec![1, 2, 3];
take_over(data);
}

Method Modifiers

Methods in extension blocks use modifiers to indicate their receiver type:

MeaningOxide SyntaxRust Equivalent
Read self (default)fn f()fn f(&self)
Mutate selfmutating fn f()fn f(&mut self)
Consume selfconsuming fn f()fn f(self)
struct Counter(value: Int)

extension Counter {
// Reads but doesn't modify (default)
fn get(): Int { self.value }

// Modifies the instance
mutating fn increment() {
self.value += 1
}

mutating fn add(n: Int) {
self.value += n
}

// Consumes the instance
consuming fn intoValue(): Int { self.value }

consuming fn intoString(): String { "Counter: ${self.value}" }
}

Transpiles to:

impl Counter {
fn get(&self) -> isize { self.value }

fn increment(&mut self) {
self.value += 1;
}

fn add(&mut self, n: isize) {
self.value += n;
}

fn into_value(self) -> isize { self.value }

fn into_string(self) -> String { format!("Counter: {}", self.value) }
}

Usage

var counter = Counter(0)
println!(counter.get()) // 0 - borrows
counter.increment() // Mutates
counter.add(5) // Mutates
let value = counter.intoValue() // Consumes - counter is moved
// counter.get() // Error: counter was moved

Borrowing Rules

Oxide follows Rust's borrowing rules:

  1. One mutable borrow OR multiple immutable borrows - never both
  2. Borrows must be valid - no dangling references
  3. Mutable references are exclusive - no aliasing
fn main() {
var data = [1, 2, 3]

// Multiple immutable borrows - OK
let a = &data
let b = &data
println!("#a #b")

// Mutable borrow - OK (immutable borrows ended)
let c = &var data
c.push(4)

// Cannot have immutable and mutable at same time
// let d = &data // Error: cannot borrow while mutably borrowed
// println!("#d")
}

Automatic Borrowing

Oxide uses automatic borrowing for function parameters. Unlike Rust, you don't write explicit &T reference types in parameter declarations:

// Oxide - parameters are borrowed automatically
fn greet(name: String) {
println!("Hello, $name!")
}

// NOT supported - explicit &T types are rejected
// fn greet(name: &String) { ... } // Error!

There are two exceptions where explicit reference types are allowed:

  1. &dyn Trait - For dynamic trait objects
  2. &var T - For mutable references
trait Display {
fn display(): String
}

// &dyn is allowed for trait objects
fn printThing(x: &dyn Display) {
println!(x.display())
}

// &var is allowed for mutable references
fn increment(x: &var Int) {
*x = *x + 1
}

Lifetime Annotations

For complex borrowing scenarios:

fn <'a> longest(x: &'a String, y: &'a String): &'a String {
if x.len() > y.len() { x } else { y }
}

struct RefHolder<'a>(data: &'a String)

fn <'a, 'b: 'a> select(short: &'a String, long: &'b String): &'a String {
short
}

Transpiles to:

fn longest<'a>(x: &'a String, y: &'a String) -> &'a String {
if x.len() > y.len() { x } else { y }
}

struct RefHolder<'a> { data: &'a String }

fn select<'a, 'b: 'a>(short: &'a String, long: &'b String) -> &'a String {
short
}

Automatic Lifetime Inference

For simple cases, Oxide automatically infers lifetimes for struct fields with reference types. You can omit the lifetime parameter when all reference fields share the same lifetime:

// Lifetime is inferred automatically
struct Parser(source: &String)

struct Pair(first: &String, second: &String)

extension Parser {
fn source(): &String {
self.source
}
}

Transpiles to:

struct Parser<'a> { source: &'a String }

struct Pair<'a> { first: &'a String, second: &'a String }

impl<'a> Parser<'a> {
fn source(&self) -> &'a String {
self.source
}
}

The transpiler:

  1. Detects reference fields without explicit lifetimes
  2. Adds a lifetime parameter 'a to the struct
  3. Propagates the lifetime to extension blocks
  4. Fills in return type lifetimes for methods returning references
When to Use Explicit Lifetimes

Use explicit lifetime annotations when you need:

  • Multiple distinct lifetimes: struct View<'a, 'b>(data: &'a T, config: &'b T)
  • Lifetime bounds: <'a, 'b: 'a> where one lifetime must outlive another
  • Complex relationships: Functions with multiple reference inputs and outputs

Capture Closures

Use capture for closures that need ownership:

fn createGreeter(name: String): fn() {
// Must capture to own name in the closure
capture { println!("Hello, $name!") }
}

fn spawnTask(data: Vec<Int>) {
// Must capture to move data into spawned task
Task capture {
for item in data {
process(item)
}
}
}

Transpiles to:

fn create_greeter(name: String) -> impl Fn() {
move || println!("Hello, {}!", name)
}

fn spawn_task(data: Vec<isize>) {
tokio::spawn(async move {
for item in data {
process(item);
}
});
}

See Also