Borrow Checking

RukaLang now tracks local borrows with a place-based checker that is smaller than Rust borrowck but follows the same safety shape for overlapping access.

Scope and Goals

  • Keep reference semantics simple for MVP.
  • Support temporary local references to named bindings, struct fields, tuple fields, indexed elements, and slice ranges.
  • Prevent overlapping mutable and shared access to the same storage region.
  • Avoid lifetime inference complexity by keeping references local-only (no storing in user data structures and no returning references).

Surface Forms Covered

  • let x = place_expr creates a shared local reference when place_expr is a place expression.
  • let &x = place_expr creates a mutable local reference.
  • Existing call-argument borrow forms remain available (&arg for &T parameters).

For non-place initializers, plain let x = expr continues to behave like a normal value initialization.

Place Model

The checker resolves borrowable expressions into a canonical place path:

  • root binding name (x)
  • zero or more projections:
    • field projection (.field / tuple index like .0)
    • index-like projection ([i] and [a..b] both normalize to one index-like projection)

Examples:

  • pair.left -> pair .field(left)
  • xs[3] -> xs .index_like
  • xs[1..3] -> xs .index_like
  • pair.left.value -> pair .field(left) .field(value)

Active Loans

Each scope keeps a list of active loans:

  • loan kind: shared or mutable
  • place path
  • owner local (the local binding that introduced the loan)

Loans are introduced by local borrow declarations and removed when the owning scope exits.

Overlap Rule

Two places overlap when:

  • they have the same root binding, and
  • their projections are not proven disjoint.

Disjointness rule used today:

  • field-vs-field at the same depth with different field names is disjoint (pair.left vs pair.right)
  • any case involving index-like projection is treated as overlapping (conservative)
  • prefix/ancestor-descendant place relations overlap

This matches Rust's conservative behavior for array/slice indexing while still allowing independent struct-field borrows.

Enforced Access Rules

  • read of a place is rejected if an overlapping mutable loan is active
  • write/move of a place is rejected if any overlapping loan is active
  • creating a shared loan is rejected if an overlapping mutable loan is active
  • creating a mutable loan is rejected if any overlapping loan is active

Current Limits

  • No index disjointness proof (xs[0] vs xs[1] is still overlapping).
  • No borrow splitting API yet (Rust-like split_at_mut equivalent not present).
  • Checker is lexical-scope based; it does not perform advanced non-lexical lifetime shortening.

These limits are intentional for MVP simplicity.