Mojo Borrowing

Borrowing lets a function temporarily access a value without taking ownership of it. The original owner retains the value after the borrow ends. Mojo enforces strict rules around borrowing to prevent data races and invalid memory access at compile time.

The Borrowing Rules

At any given moment, for a single value, you may have EITHER:

  Rule A: Any number of immutable (read-only) borrows
          ────────────────────────────────────────────
          reader1 ──borrow──→ value
          reader2 ──borrow──→ value    ← both can read simultaneously
          reader3 ──borrow──→ value

  OR

  Rule B: Exactly ONE mutable borrow, and NO other borrows
          ────────────────────────────────────────────────
          writer ──inout──→ value      ← sole accessor during this time

  Never both A and B at the same time.

This rule guarantees that no two parts of your code disagree about the current state of a value. Python has no such guarantee — concurrent modifications can corrupt data silently.

Immutable Borrows (borrowed)

Pass a value as borrowed to read it without copying or transferring it. Multiple functions can borrow the same value simultaneously.

fn word_count(borrowed text: String) -> Int:
    var count = 0
    var in_word = False
    for i in range(len(text)):
        var ch = text[i]
        if ch == " " or ch == "\n":
            in_word = False
        else:
            if not in_word:
                count += 1
                in_word = True
    return count

fn char_count(borrowed text: String) -> Int:
    return len(text)

fn main():
    var essay = "Mojo is fast and safe"
    # Both functions borrow 'essay' — no conflicts
    print("Words:", word_count(essay))   # Words: 5
    print("Chars:", char_count(essay))   # Chars: 21
    print(essay)                          # still valid

Mutable Borrows (inout)

Pass a value as inout to let the function modify it. While the mutable borrow is active, no other borrows of the same value exist.

fn normalize(inout values: List[Float64]):
    var total: Float64 = 0.0
    for i in range(len(values)):
        total += values[i]
    if total == 0.0:
        return
    for i in range(len(values)):
        values[i] = values[i] / total

fn main():
    var weights = List[Float64](10.0, 20.0, 30.0, 40.0)
    normalize(weights)
    for i in range(len(weights)):
        print(weights[i], end=" ")   # 0.1 0.2 0.3 0.4
During normalize() call:
  main() ──inout──→ weights   (main cannot read/write weights while normalize runs)
                  ↑
            sole accessor

After normalize() returns:
  main() reclaims ownership of weights

Borrow Checking Example: Compile-Time Safety

The Mojo compiler rejects code that would create conflicting borrows. This prevents entire categories of bugs before the program ever runs.

fn main():
    var data = List[Int](1, 2, 3)

    # This pattern is safe — sequential borrows, not simultaneous:
    var first = data[0]     # borrows data briefly, then releases
    var second = data[1]    # borrows data briefly, then releases
    print(first, second)    # 1 2

    # The compiler prevents simultaneous mutable + immutable borrows:
    # You cannot hold an inout reference while also reading the same value

Returning Borrowed References

A function can return a reference into data it borrowed, but the lifetime of that reference must not exceed the lifetime of the original value. Mojo tracks this automatically.

struct DataStore:
    var buffer: List[Int]

    fn __init__(inout self):
        self.buffer = List[Int](10, 20, 30, 40, 50)

    fn first(self) -> Int:
        return self.buffer[0]

    fn last(self) -> Int:
        return self.buffer[len(self.buffer) - 1]

fn main():
    var store = DataStore()
    print(store.first())   # 10
    print(store.last())    # 50

Borrow vs Copy — When Each Happens

Situation                           | What Mojo Does
------------------------------------|---------------------------
fn f(borrowed x: T)                 | Zero-copy reference
fn f(inout x: T)                    | Zero-copy mutable reference
fn f(owned x: T) + caller passes x^ | Move (zero-copy transfer)
fn f(owned x: T) + caller passes x  | Copy (if T is Copyable)
var b = a (small Int, Float64)      | Copy
var b = a^ (String, List, etc.)     | Move

Practical Pattern: Read-Many, Write-Once

struct Config:
    var debug: Bool
    var max_retries: Int
    var timeout: Float64

    fn __init__(inout self, debug: Bool, retries: Int, timeout: Float64):
        self.debug = debug
        self.max_retries = retries
        self.timeout = timeout

fn log_config(borrowed cfg: Config):
    print("Debug:", cfg.debug)
    print("Retries:", cfg.max_retries)
    print("Timeout:", cfg.timeout)

fn set_debug(inout cfg: Config, value: Bool):
    cfg.debug = value

fn main():
    var cfg = Config(False, 3, 30.0)
    log_config(cfg)        # immutable borrow
    set_debug(cfg, True)   # mutable borrow
    log_config(cfg)        # immutable borrow again

Key Takeaways

Borrowing gives temporary access to a value without transferring ownership. Immutable borrows (borrowed) allow multiple simultaneous readers. A mutable borrow (inout) is exclusive — no other borrows exist at the same time. Mojo enforces these rules at compile time, eliminating data races and use-after-free errors. Borrows are zero-cost — no copying or heap allocation happens. The borrow checker is what allows Mojo to be both memory-safe and garbage-collector-free.

Leave a Comment

Your email address will not be published. Required fields are marked *