← Back to AILP Home

Return-Borrow Methods ($$i / $$m returns + from <ident> clause)

This chapter walks through the W-13 surface that landed across v0.31.8.0 – v0.31.8.5: how to write trait methods (and free functions) that return a borrow, how the compiler decides which parameter the returned borrow "comes from", and how the new from <ident> clause gives you a precise override when more than one borrow parameter is in play.

The whole feature is compile-time only. At runtime, $$i T and $$m T returns are just pointers. The borrow checker uses the declared shape of the function (its parameter borrows, its return qualifier, and any from <ident> clause) to figure out which local storage in the caller must outlive the returned binding.

This is a Phase-6-style addendum to the traits cookbook. It assumes you have read basic.md and borrow.md.

1. Why a return-borrow needs a source

A free function or trait method that returns a $$i or $$m borrow gives the caller a handle into something the function did not own. That something has to live in storage the caller can see; otherwise the borrow would dangle the moment the function returned. The compile-time job of the borrow checker is to tie the returned binding to a specific parameter so the caller's storage for that argument stays pinned for the duration of the binding.

In v0.25.4 the single-borrow-parameter case was already handled with an implicit rule: "if exactly one parameter is a borrow, that's the source." v0.31.8.x generalizes that rule for two or more borrow parameters, and adds an explicit override (from <ident>) so you can tie the return to any specific parameter you like.

2. The single-borrow-parameter default (no clause needed)

struct:Pair = { int32:a; int32:b; };

func:get_a = $$i int32($$i Pair:p) {
    pass p.a;
};

func:failsafe = int32(tbb32:err) { exit 7; };

func:main = int32() {
    Pair:q;
    q.a = 42i32;
    $$i int32:y = raw get_a(q);
    exit 0;
};

There is only one borrow parameter (p), so the borrow checker ties y back to q (the argument bound to p). For the lifetime of y, q is treated as borrowed: another $$m q.a in the same scope would fail with ARIA-026.

3. The multi-borrow default

When more than one parameter is a borrow, the v0.31.8.2 default rule is:

struct:Pair = { int32:a; int32:b; };

func:take_either = $$i int32($$i Pair:l, $$i Pair:r) {
    pass l.a;
};

Here take_either has two borrow parameters but no from clause, so the default picks l. A caller's l-argument storage is what gets pinned for the duration of the returned binding.

4. Overriding the default with from <ident>

The default is convenient but sometimes wrong. The from <ident> clause (TRAIT-RET-DEC-003) is the precise override: it says explicitly which parameter the returned borrow comes from. It is parsed right after the parameter list and before the function body:

struct:Pair = { int32:a; int32:b; };

func:take_either = $$i int32($$i Pair:l, $$i Pair:r) from r {
    pass r.a;
};

With from r, the returned binding ties to the second argument, not the first. A caller may then mutate l while the returned binding is live, but mutating r would fail with ARIA-026.

from is a contextual keyword (TRAIT-RET-DEC-004): bare from remains a perfectly legal identifier outside this position, so no existing code is invalidated.

5. Trait methods: the same rules apply

trait:GetA = {
    get_a: $$i int32($$i self);
};

struct:Pair = { int32:a; int32:b; };

impl:GetA:for:Pair = {
    get_a: $$i int32($$i Pair:self) {
        pass self.a;
    };
};

Trait method signatures accept the same $$i / $$m return qualifiers and the same optional from <ident> clause as free functions. For a single $$i self receiver, the source is self by the single-borrow default. For multi-borrow trait methods, the self-preference rule (TRAIT-RET-DEC-005) kicks in unless an explicit from clause overrides it.

6. dyn Trait cannot dispatch return-borrow methods

Dynamic dispatch through a dyn Trait fat pointer erases the concrete receiver identity that the borrow-source rule needs. Calling a return-borrow method through a dyn receiver is therefore rejected at type-check time with ARIA-053 (TRAIT-RET-DEC-007):

$$i dyn GetA:d = p;
int32:y = d.get_a();
//        ^^^^^^^^^
// ARIA-053: cannot call return-borrow method 'get_a' on trait
// object 'dyn GetA': dynamic dispatch erases the receiver identity
// needed to tie the returned borrow's lifetime. Call through a
// concrete impl, or change the trait method to return by value.

Non-borrow methods on dyn Trait are unaffected. Concrete-impl calls (p.get_a() where p:Pair) work normally.

7. Diagnostic table

The from clause and the return-borrow surface define four borrow-checker diagnostics today:

Code Severity Trigger
ARIA-023 error from <ident> names a parameter that is not a borrow.
ARIA-053 error dyn Trait dispatch of a method whose return is $$i/$$m.
ARIA-054 warn from <ident> on a by-value return — clause has no effect.
ARIA-055 error from <ident> names a parameter that does not exist.

ARIA-054 is a warning so you can still compile and run the program; the compiler simply ignores the meaningless clause. ARIA-055 is the dedicated "typo / wrong name" diagnostic (split off from ARIA-023 in v0.31.8.4, TRAIT-RET-DEC-008).

8. What is not yet supported

9. Quick reference

You write … The checker ties the return to …
one $$i/$$m param, no from that param
two+ borrow params, no from, free fn the first borrow param
two+ borrow params, no from, trait method self if present, else the first borrow param
any signature with from <ident> the param named <ident> (must exist + be borrow)
from <ident> on a by-value return nothing — ARIA-054 warns and ignores the clause

Bug fixtures

For runnable examples that exercise each rule: