Ruta graveolens  ·  notes from a language experiment  ·  cultivated since 2025

Borrow Accessors

A borrow accessor is a method that hands out a second-class borrow of a projection of its receiver: v.get_ref(i) produces a borrowed place naming element i in place — no copy, no move-out — checked by the ordinary law-of-exclusivity loan machinery and scoped to the enclosing full expression (core calculus docs/formal/01-core-calculus.md §5.8, rule (Accessor-Call)). The same accessor form also supports mutable accessors: v.get_mut(i) produces an exclusive place, and uses inout self with an -> inout T result.

Declaration

accessor    = "fn" IDENT "(" accessor_self [ "," params ] ")"
              "->" accessor_result type "{" { statement } yield_expr [ ";" ] "}" ;
accessor_self   = "borrow" "self" | "inout" "self" ;
accessor_result = "borrow" | "inout" ;
yield_expr  = "yield" expression ;

A -> borrow or -> inout result position and the yield form are stable accessor syntax. Programs using either are checked by the declaration and call-site legality rules below.

An accessor MUST pair its result and receiver modes exactly: borrow self with -> borrow T, or inout self with -> inout T. A result on a free function, an associated function, or a by-value or mut self method is rejected (E0257); user-defined anonymous-struct accessors are also rejected. The trusted standard-library anonymous collection path is the narrow exception needed by ArrayBuf(T) and remains subject to the declaration and call-site gates below. A mismatched pair reports the required pairing.

Accessor value parameters MUST be plain by-value parameters: borrow, inout, and comptime parameter modes are rejected on an accessor (E0260).

The accessor body

Every non-diverging path through an accessor body MUST fall through to the body's single trailing yield: the final statement of the body is a yield, no other yield may appear, no code may follow it, and the body MUST NOT contain return or ? (E0254). Guard code before the yield may only diverge — trap or @panic — or fall through. A yield outside an accessor body is rejected (E0256).

The operand of the yield MUST be a place rooted at the receiver parameter: self, or a projection chain from self through fields, indices, or nested accessor calls (E0255). Yielding a local, a parameter other than the receiver, or a computed value would hand out a place that dies with the accessor's guards.

The trusted standard-library ArrayBuf(T).get_ref accessor is the narrow representation-level exception. It may use the checked pointer-to-place bridge yield checked { @place(@ptr_offset(...)) }; after its bounds guard. This bridge is recognized only for a trusted standard-library accessor whose pointer base is a field chain rooted at self; user code may not use @place to manufacture an arbitrary accessor result. Other owning collections compose this canonical accessor rather than duplicate the privileged bridge. Its defining equation is i < len -> view⟨A | i, 1⟩ and i >= len -> bounds trap (ADR-0062, RUE-1017).

Calls

A call to an accessor requires its receiver to be a place, exactly as passing it as a borrow or inout argument does (6.4:27), and evaluates its arguments by value. A -> borrow result is a borrowed place, not a first-class value: a shared loan on the receiver's root variable. A -> inout result is an exclusive place and requires the receiver to be mutable, with the same addressability and mutability rules as an inout argument. Both loans extend through the enclosing full expression (core calculus docs/formal/01-core-calculus.md §5.8, rule (Accessor-Call)).

An accessor result MUST NOT escape its full expression: returning it (E0250), storing it by assignment (E0251), binding it with a plain let (E0252), or capturing it in a struct or array literal (E0253) is rejected. A join yields whatever its arms yield, so an if/else arm, a match arm, and a block's tail expression each pass the property along: when any arm can yield an accessor result, the join is an accessor result of that arm's root, and every rule above applies to it unchanged — let b = if c { v.get_ref(i) } else { 0 }; is the same E0252 as let b = v.get_ref(i);. Consuming the join within its own full expression stays legal, exactly as consuming the call directly does.

The law of exclusivity extends over the accessor loan's whole extent: shared accessor results may coexist with one another, while an exclusive accessor result conflicts with every shared or exclusive access to the same root, and an exclusive use conflicts with every active accessor loan. An exclusive use of the borrowed root — passing it inout, an inout self receiver access, assigning to it, or moving it — anywhere within the same full expression is rejected (E0259). use(v.get_ref(i), g(inout v)) is ill-formed even though the read syntactically precedes the exclusive access. When the accessor result is itself re-borrowed as a borrow argument of the same call (use(borrow v.get_ref(i), inout v)), the conflict is caught by the general argument-exclusivity rule and surfaces as its diagnostic (E0430) rather than E0259; the program is rejected either way. A loan carried out of a join (6.6:9) keeps this extent, so use(if c { v.get_ref(i) } else { 0 }, g(inout v)) is rejected on the same footing as the unwrapped call.

Reading a value that owns resources (one with drop glue) out of an accessor result by value is rejected (E0258): the result is a borrow, not an owner, and a by-value read would mint an aliasing second owner — the same soundness argument as the by-copy container-read gate (E0711). Only trivially droppable values may be read out; owning values are used in place through projection, borrow arguments, comparison, or borrow self methods.

Dynamics and lowering

An accessor call evaluates by the accessor's inlined body: the guards run in the calling context — and may trap — and the call's result is then the yielded place itself, projected from the caller's receiver place (core calculus §5.8 dynamics note). No function call occurs at runtime.

Accessors are required-inlineable by design: no calling convention for "returning a place" exists, which is the forward-compatibility contract that keeps the future coroutine-accessor generalization (RUE-1012) free to choose its own call shape.

Accessor expansion MUST be acyclic: an accessor body MUST NOT call an accessor whose expansion encloses it, whether directly (fn xr(borrow self) -> borrow i64 { yield self.xr(); }) or through a chain of other accessors (E0261). Because the call is the inlined body (6.6:12), a cycle has no finite expansion — a property of the declarations alone, so a cycle whose every link is a call on the accessor's own self receiver is rejected at the declaration, whether or not anything calls an accessor in it, like the other legality rules of this chapter. A re-entrant call reached through any other receiver (a by-value guard of the owner's own type) is rejected when a call site demands the body's analysis and expansion. The rejection names the recursive accessor either way.

An exclusive accessor result is expression-scoped and may be used as a place: v.get_mut(i) = value, projected assignment such as v.get_mut(i).field = value, and set(inout v.get_mut(i)) are valid when the receiver is mutable. The right-hand side is evaluated first; when the yielded destination owns a droppable value, its old value is dropped before the new value is stored. Ordinary linear overwrite checks apply to the yielded place; an overwrite of a live linear value is rejected unless reinitialization is provable.

An accessor result MUST NOT escape its expression-scoped loan. It cannot be returned, captured in an aggregate, or assigned as a value. A mutable accessor cannot be called through an immutable or shared-borrowed receiver, and two mutable results (or a mutable result and a shared result) rooted at the same receiver are rejected in either evaluation order.

Accessor exclusivity is root-granular in this phase. Path-granular disjointness, coroutine accessor bodies, and Option(inout T) results are outside scope.