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:
| Meaning | Oxide Syntax | Rust Equivalent | Call Site |
|---|---|---|---|
| Read-only (default) | x: T | x: &T | f(x) - auto & |
| Mutable borrow | inout x: T | x: &mut T | f(&x) - explicit & |
| Take ownership | consuming x: T | x: T | f(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:
| Meaning | Oxide Syntax | Rust Equivalent |
|---|---|---|
| Read self (default) | fn f() | fn f(&self) |
| Mutate self | mutating fn f() | fn f(&mut self) |
| Consume self | consuming 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:
- One mutable borrow OR multiple immutable borrows - never both
- Borrows must be valid - no dangling references
- 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:
&dyn Trait- For dynamic trait objects&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:
- Detects reference fields without explicit lifetimes
- Adds a lifetime parameter
'ato the struct - Propagates the lifetime to extension blocks
- Fills in return type lifetimes for methods returning references
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);
}
});
}