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:
- Trait methods: prefer a
selfborrow (TRAIT-RET-DEC-005). - Free functions: prefer the first borrow parameter (TRAIT-RET-DEC-002).
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
- Cross-module summary plumbing. The borrow-source decision is
computed during the BorrowChecker pass and lives in an in-memory
func_summariesmap. There is no.npksummarysidecar yet, so a caller in module B cannot infer the return-borrow source of a function declared in module A; the diagnostic "summary unavailable (cross-module return-borrow inference is deferred)" still fires for that case. This is on the roadmap for v0.31.9.x. - K-Framework runtime modeling of
$$i Treturns. K'sFuncDeclgrammar accepts$$i/$$mon parameters but not yet on return types. K core tests 155–157 lock the runtime value-flow of the single-source, explicit-from r, andself-default paths, but the in-K aliasing semantics are an open extension item.
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:
- bug488 – parser accepts
from rafter$$ireturn. - bug490 – bare
fromstill works as an identifier. - bug491 – missing identifier after
from→ clear parse error. - bug492 – explicit
from roverrides the multi-borrow default. - bug493 – named source blocks a
$$mon the same storage (ARIA-026). - bug494 –
from <unknown>rejected (ARIA-055). - bug495 –
from <non-borrow-param>rejected (ARIA-023). - bug496 –
dyndispatch of a return-borrow method rejected (ARIA-053). - bug497 – concrete impl call to the same method still works.
- bug498 –
dyndispatch of a non-borrow method still works. - bug499 –
from ron a by-value return warns (ARIA-054) and runs. - bug500 –
from <typo>on a single-borrow function rejected (ARIA-055).