Borrows across await
The v0.31.0.4 borrow checker rejects every live $$i and
$$m borrow at an await suspension point. This page is the
contract.
The rule
At each
awaitsite, every live borrow is invalidated. Reading or writing through that borrow after theawaitis a compile-time error.
The diagnostic:
ARIA-041: <flavor> borrow of '<host>' does not survive an
'await' suspension point; release the borrow before awaiting,
or take a Handle<T> for a cross-await reference
where <flavor> is immutable ($$i) or mutable ($$m)
and <host> is the source binding.
Why
A borrow's invariants — no overlapping mutation, the source
not destroyed, the source not moved — must hold for the
entire lifetime of the borrow. Across an await:
- Other tasks may run on the executor.
- The GC may collect and move heap objects.
- Arenas the source lives in may be destroyed by a destroying call from another task.
- The stack frame holding the source survives (it's part of the coroutine frame) but its contents are no longer under the borrow checker's local control.
The conservative answer that always works is: borrows end at
the suspension point. Code that needs a stable reference
across an await takes a Handle<T> (ARIA-032-protected
across destroying calls — see
guide/handles/) or copies the value.
Example: rejected
async func:add_one = int32() { pass 1i32; };
async func:bad = int32() {
int32:x = 10i32;
$$m int32:bx = $$m x; // mutable borrow taken
int32:y = raw await add_one(); // ARIA-041: $$m bx invalidated
bx = bx + y; // ARIA-022: bx no longer valid
pass bx;
};
The compiler emits one ARIA-041 per live borrow at the
await. If the same await site has multiple live borrows
(e.g. an $$i borrow of a and an $$m borrow of b),
each gets its own diagnostic. Regression bug347 produces
two ARIA-041 errors at a single await (multi-borrow).
After the await the borrows are released, so the
following code can re-borrow if it likes — the borrow checker
will not double-report. The original binding (bx above) is
still gone, however; you would have to take a fresh borrow:
async func:fixed = int32() {
int32:x = 10i32;
int32:y = raw await add_one(); // no live borrow at the await
$$m int32:bx = $$m x; // fresh borrow after resume
bx = bx + y; // OK
pass bx;
};
Example: accepted
async func:add_one = int32() { pass 1i32; };
async func:good_drop_borrow = int32() {
int32:x = 10i32;
{
$$m int32:bx = $$m x;
bx = bx + 1i32; // borrow used and released
}
int32:y = raw await add_one(); // no live borrow at the await
pass x + y;
};
async func:good_handle = int32() {
Handle<int32>:h = HandleArena.put(10i32); // ARIA-032 protected
int32:y = raw await add_one(); // OK across await
int32:v = HandleArena.get(h);
pass v + y;
};
The two patterns:
- Scope the borrow. Open an inner block, take the borrow,
use it, and let the inner
}release it before theawait. - Use a
Handle<T>. Handles are arena-mediated and the ARIA-032 path already protects them across destroying calls. They surviveawait.
Both $$i and $$m are rejected
This is a deliberate v0.31.0.4 / D-5 simplification. A more relaxed rule (e.g. "shared immutable borrows survive if no other task can mutate the source") was considered and rejected for Phase 1: it requires interprocedural reasoning about what other tasks might do, which the per-function borrow checker is not set up for. Rejecting both flavours is the conservative answer that always works.
A future cycle may relax this for the cases where the borrow checker can prove non-interference. For now: same rule, both flavours, unconditional.
Supersedes v0.21.2 ARIA-030
The v0.21.2 cycle landed ARIA-030 as a warning for
$$m-live-at-await (and let $$i through silently). That
warning is gone as of v0.31.0.4 — the same situations now
fire ARIA-041 as a hard error, and the immutable case is
no longer special-cased.
Code that previously compiled with an ARIA-030 warning may
stop compiling. The fix is the same as for the new errors:
scope the borrow tighter, or take a Handle<T>.
Three pre-existing fixtures were updated to reflect the new rule:
run_bug_tests_0212.sh::bug089— was ARIA-030 warning, now ARIA-041 error.run_bug_tests_0212.sh::bug091— same.run_bug_tests_0256.sh::bug204— same.
What is not rejected
These are accepted at the await site because nothing live
is borrowed across it:
- A bare local variable read after the
await(the local binding survives; only borrows die). - A
Handle<T>value held across theawait. - A
Future<T>(internal — you don't name them anyway). - A method call on
selfafter theawait, unlessselfwas reached via a$$m/$$iborrow that was live at the suspension point.
Regression coverage: bug343 / bug344 / bug347 / bug349
(must fail with ARIA-041); bug345 / bug346 / bug348 /
bug350 (must compile and run — the accepted shapes above).