← Back to AILP Home

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 await site, every live borrow is invalidated. Reading or writing through that borrow after the await is 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:

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:

  1. Scope the borrow. Open an inner block, take the borrow, use it, and let the inner } release it before the await.
  2. Use a Handle<T>. Handles are arena-mediated and the ARIA-032 path already protects them across destroying calls. They survive await.

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:

What is not rejected

These are accepted at the await site because nothing live is borrowed across it:

Regression coverage: bug343 / bug344 / bug347 / bug349 (must fail with ARIA-041); bug345 / bug346 / bug348 / bug350 (must compile and run — the accepted shapes above).