Skip to content

Ownership & Borrowing

While the programmer experiences Wysna as a pure value-semantics language, the compiler’s Mid-Level IR (MIR) actively optimizes parameter passing through borrowing elision and provides explicit weak references for breaking cyclic class structures.

When you pass a managed collection or a nominal record into a function, the compiler automatically marks the parameter as borrowed at the ABI level:

fn compute_sum(items: list[int]) -> int {
let mut total = 0
for n in items {
total = total + n
}
return total
}

In the generated C99 code:

  • The caller does not increment the reference count before invoking compute_sum.
  • compute_sum receives a raw borrowed pointer.
  • compute_sum does not decrement the reference count upon exit.

This provides the optimal balance: the ergonomics and safety of passing by value, coupled with the raw CPU register speed of C pointer passing.


When a function creates and returns a newly constructed object, it passes ownership back to the caller:

fn generate_sequence(size: int) -> list[int] {
let mut numbers: list[int] = []
for i in 1..=size {
numbers.append(i * 10)
}
return numbers # Ownership is transferred to caller
}
fn main() -> unit {
let seq = generate_sequence(5)
print("Sequence:", seq)
}

The compiler’s MIR tier assigns ownership to seq in main(). When main() terminates, the compiler emits the cleanup call that releases seq.


Because pure value types (struct, list, dict) are strictly acyclic, they never form cycles. However, reference types (class) can reference each other cyclically.

To prevent reference cycles from leaking memory, Wysna provides first-class weak reference handles:

  • weak(object): Creates a non-owning Weak[C] handle to a class instance without incrementing the strong reference count.
  • upgrade(handle): Attempts to promote the weak reference to a strong reference, returning C?. If the target object was already deallocated, it safely evaluates to none.
class Node {
public id: int
public parent: Weak[Node]?
public child: Node?
fn init(id: int) {
this.id = id
this.parent = none
this.child = none
}
}
fn main() -> unit {
let parent = Node(1)
let child = Node(2)
# Establish parent-child relationship:
parent.child = child
child.parent = weak(parent) # Non-owning back-reference
# Accessing the parent from child safely:
let maybe_parent: Node? = upgrade(child.parent ?? weak(parent))
match maybe_parent {
Some(p) => print("Parent ID:", p.id),
None => print("Parent not available"),
}
}

When parent goes out of scope, its strong reference count drops to zero and it is immediately deallocated. Any subsequent call to upgrade(child.parent) returns none deterministically, avoiding dangling pointers and use-after-free bugs.