Cone Language Reference

Awaiting a Reply

On this page

Note: Not built yet: an await in a few places within an expression, a time limit on an await, cancelling a paused behaviour, waiting for whichever of several futures arrives first, and a way to look at a future without opening it.

Awaiting a behaviour's reply#

A message is one-way: the sender goes on at once, and whatever the behaviour returns is dropped. Inside an actor's own behaviour, await sends a message and waits for what the behaviour it sends returns:

struct Conn {
  pub url u64;
  fn final(self &uni) {say("  close", url);}
}

struct Buffer {
  pub size u64;
}

actor Net {
  pub async do read(self, url u64) Buffer {
    say("  read", url);
    new Buffer(url * 100u64);
  }
}

actor Fetcher {
  net Net;

  pub async do fetch(self, url u64) Buffer {
    imm conn = new Conn(url);             // opened
    imm buf = await net.read(conn.url);   // conn waits, with the method
    say("  fetched", buf.size);
    buf;                                  // the reply to the 'await' on fetch
  }                                       // conn closes here
}

actor Page {
  fetcher Fetcher;
  body u64;

  pub async do load(self, u u64) {
    say("load", u);
    imm b = await fetcher.fetch(u);       // pauses here
    body = b.size;                        // and resumes when the reply comes
    say("loaded", body);
  }
}

(say prints a word and a number on a line.) Sent page.load(7u64), this prints:

load 7
  read 7
  fetched 700
  close 7
loaded 700

When load reaches its await, it sends fetch to the fetcher and pauses. The worker running Page does not wait with it: Page goes back to its mailbox, and its other messages run meanwhile. When fetch returns its Buffer, the value comes back to Page as a reply, and load resumes just after the await, whose value it is. Fetcher does the same at its own await: while Net reads, Fetcher is free to handle other fetches. Its connection, conn, is still open while it waits, and closes when fetch ends, after the reply came, just as it would if read had been an ordinary call.

An I/O operation is awaited the same way, its answer coming back as a reply does: a real read of a socket is await conn.read(buf) (Awaiting I/O).

Await in a behaviour#

An await stands only in a behaviour: not in a plain function or a struct's method, and not in any fn of an actor's — its init, which runs on the thread making the actor, before the actor takes any message; its final, as it dies; or a helper, which runs inside the method that calls it and cannot pause partway through the call. A behaviour may hold any number of them. The behaviour's declaration says it may pause, and that is as far as the marking goes: a caller does not await a behaviour in order to call it, since the code that runs it is the actor's dispatcher, at the top of its worker's stack, and every message already runs later than it was sent. So an await pauses only the behaviour it is written in, and goes no further, where in C#, Rust and JavaScript a caller that waits for an asynchronous function's result must be asynchronous itself, and its callers in turn, all the way up the stack.

The awaits are at the top of an actor's work, in its behaviours, and the synchronous work between them is pushed down, onto a helper or a type the actor owns:

actor Reporter {
  clock Clock;
  last u64;

  pub async do report(self) {
    imm t = await clock.now();    // the 'await', at the top of the behaviour
    last = self.format(t);        // the synchronous work, pushed down
  }

  fn format(self &, t u64) u64 {
    t * 1000u64 + last;
  }

  fn stamp(self) {
    last = await clock.now();     // refused: stamp is a 'fn', synchronous
  }
}
'await' stands only in an actor's behaviour, declared 'async do', where the behaviour is
cut and returns to the actor's dispatcher to wait. This one is in stamp, a 'fn' of actor
Reporter's: synchronous, run inside the method that calls it, which cannot pause partway
through a call. An 'await' belongs at the top of a behaviour: await there, and push the
synchronous work down, onto a helper like this one or a type the actor owns, to run
between the awaits.

What an await does#

  1. It asks. What follows await is a behaviour of an actor that returns a value. It is sent, as any message is, and the request carries where the reply goes. (Or it is a future, a request sent earlier: Storing a future.)
  2. The behaviour pauses. It returns to its actor's dispatcher. What the rest of the behaviour needs waits in the actor (below), not on a stack, so the worker goes on to other work.
  3. Other messages run. The actor handles the rest of its mailbox meanwhile, one message at a time, as always, messages it sends itself among them. So its state may have changed by the time the behaviour resumes: what it read from its fields before the await, it reads again after.
  4. The reply resumes it. When the behaviour asked returns, its value comes back to the actor that awaited, as a message in its mailbox. When that message's turn comes, the rest of the behaviour runs, with the value as the await's, perhaps on another worker thread.

It reads as one scope#

The method runs in parts, with other messages between them, but it is still the one method written, with one scope. Every value it holds when it pauses dies where it would if the method had never paused, and in the same order:

struct Note {
  pub n u64;
  fn final(self &uni) {say("  drop", n);}
}

actor Clock {
  pub async do now(self) u64 {42u64;}
}

actor Logger {
  clock Clock;

  pub async do log(self) u64 {
    imm first = new Note(1u64);
    imm second = new Note(2u64);
    imm t = await clock.now();
    say("  at", t + first.n);             // 'second' is not named again
    t;
  }                                       // second dies, then first
}

logger.log() prints at 43, then drop 2, then drop 1. second is never named after the await, yet it waits too, and dies at the method's end, the last declared first, as it would in a method with no await. Nothing is dropped early at an await, so what a finalizer does (closing a file, giving back a buffer) happens where the method's text says. An await returns to the dispatcher, not to the author: the author reads one method, and the pause shows only in when the rest of it runs, and in what the actor's other messages did meanwhile.

What crosses an await#

  • Values move. Every value the method still needs after the await, and every value with a finalizer it still holds, waits for the reply, and comes back to the same local when the method resumes. It is moved, never copied: a counted owner (an Arc, an actor's handle) is not counted again, and a value that may only move waits like any other. A local that is not used again and has nothing to do as it dies is simply left behind.
  • Borrows end. Every borrowed reference the method holds ends at the await, as if its scope ended there: while the method is paused, other messages run, and may change or drop what was borrowed. Holding one at an await is no error; using one after it is (next).
  • A 'static borrow does not end. A borrow of a global may already be returned from a function, since what it points at outlives every function; for the same reason it crosses an await:
    imm limit u64 = 100u64;
    
    actor Gauge {
      clock Clock;
    
      pub async do check(self) u64 {
        imm max = &limit;                     // a borrow of a global: 'static
        imm t = await clock.now();
        *max - t;                             // still valid after the 'await'
      }
    }
    
  • A lock is given back. A borrow through a lock permission holds its lock for as long as the borrow lasts, so the borrow ends at the await and the lock is given back there. To reach the value again, the method borrows it again:
    struct Count {pub n u64;}
    
    actor Tally {
      count Arc[Mutex, Count];
      clock Clock;
    
      pub async do bump(self) u64 {
        imm c = &mut *count;                  // takes the lock
        c.n += 1u64;
        imm t = await clock.now();            // gives it back
        (&mut *count).n += t;                 // takes it again, for the statement
        (&*count).n;
      }
    }
    
    A lock borrow is never 'static, since it borrows a hidden local that holds the lock, so it never crosses: no method holds a lock while it waits.
  • A reference into the actor's own region is kept. The actor's collector keeps it: a paused behaviour's record, parked in the pending table, is one of its roots (The actor's own garbage-collected region). (The collector package's Gc is refused inside an actor altogether: Inside an actor.)
  • The state is lent afresh. self and the actor's fields may be used after an await: the dispatcher lends the state again when the method resumes. A borrow made through self before the await ends, as any borrow does.

Using a borrow after an await#

A borrow used after the await that ended it is refused, with the error any borrow used after its end gets, pointing at the await as where it ended:

actor Meter {
  clock Clock;
  reading u64;

  pub async do stale(self) {
    imm r = &reading;
    imm t = await clock.now();  // refused: r's borrow ends here, and r is used below
    reading = *r + t;
  }

  pub async do fresh(self) {
    imm t = await clock.now();
    imm r = &reading;           // borrowed after the 'await'
    reading = *r + t;
  }
The borrow 'r' holds of '*self' (made at ..) ends at this 'await', where the method
returns to its actor's dispatcher to wait, and 'r' is used again at ..

A lock borrow used after the await is refused the same way, the message naming the lock it held.

Await inside a statement#

An await may stand inside a larger expression. What follows await is worked out first, up to the request it sends; then the behaviour pauses, and the rest of the statement runs when the reply comes, as the first thing the behaviour does when it resumes:

actor Counter {
  pub async do next(self) u64 {2u64;}
}

actor Shelf {
  counter Counter;
  slots Array[u64, 4];
  total u64;

  fn put(self, n u64) {total = total + n;}

  pub async do stock(self) u64 {
    slots[await counter.next()] = 7u64;   // slots is reached after the 'await'
    self.put(await counter.next());       // and so is self, put's receiver
    total += await counter.next();        // total is read and written after it
    slots[2] + total;
  }
}

await shelf.stock() is 11. Each statement runs as if the value awaited were made first, into a local of its own: imm i = await counter.next(); slots[i] = 7u64;. So what the rest of the statement reaches — slots, the state through self, total — it reaches as it is when the behaviour resumes, after whatever the actor's other messages did meanwhile, and nothing of it is borrowed while the behaviour waits. A borrow written to the left of the await is taken then too: add(&x, await clock.now()) borrows x once the reply has come, and total += await counter.next() loses nothing another message added to total while it waited. Rust holds such a borrow across its await instead, which it can because nothing else may change what an exclusive borrow reaches while it waits; inside an actor, its other messages run meanwhile.

A value is not reached again. One made to the left of the await, in the order the statement is worked out — an argument before it, an operator's operand — is made before the request is sent and waits with the method: in total + await counter.next(), total is read before the await. The value an assignment stores is made before its place is reached, so in slots[await counter.next()] = f(), f runs first.

What is reached again is a plain path: a local, self, a field, an element at a plain index, or a borrow of one. A borrow a call made cannot be made again, so a borrow, or a place, made by a call to the left of the await is refused, and the message says to bind what the call makes to a local first. In Meter, where add is a function taking a &u64 and a number, and compute one returning a number:

  pub async do fromCall(self) {
    reading = add(&compute(), await clock.now());  // refused: compute() made what is borrowed
  }

  pub async do bound(self) {
    imm c = compute();
    reading = add(&c, await clock.now());          // c is borrowed after the 'await'
  }
This borrow, written to the left of an 'await' in the same statement, is made by a call
or a temporary, and is used after the 'await'. The rest of the statement runs after the
'await', which ends every borrow: only a plain path -- a local, 'self', a field, an
element at a plain index -- is reached again there, never what a call made. Bind what
the call makes to a local first, and use the local here.

More than one await may stand in a place, in both places a swap exchanges, in a slice's bounds, in the places of a parallel assignment and among the entries a <- appends. Each is worked out first, from left to right, every await once: the indexes of the place, those of the second side of a swap after those of the first, and a parallel assignment's value before its places. An index worked out waits with the method while the ones after it are awaited, and the arrays and slices they index are reached after the last reply, as they are with one await. The entries of a <- written before the last one that holds an await are made before the list is borrowed, in the order written, and appended after it:

actor Lane {
  pub async do pick(self, n u64) u64 {n;}
}

actor Board {
  lane Lane;
  cells Array[Array[u64, 3], 2];
  marks List[u64];

  pub async do fill(self) u64 {
    cells[await lane.pick(1u64)][await lane.pick(2u64)] = 5u64;                 // cells[1][2]
    cells[await lane.pick(0u64)][0u64] <=> cells[await lane.pick(1u64)][2u64];  // both sides
    (cells[0][1], cells[await lane.pick(1u64)][await lane.pick(1u64)]) = (7u64, 8u64);
    marks <- (cells[1][2], await lane.pick(9u64), 3u64);
    imm row = &cells[await lane.pick(1u64)][await lane.pick(0u64)..<await lane.pick(2u64)];
    row.len() * 100u64 + cells[0][0] * 10u64 + *marks[1];
  }
}

await board.fill() is 259.

One request, one reply#

An await sends one request and takes exactly one reply. A message that returns a value answers once, when it returns, from whichever path it returns by:

actor Cache {
  net Net;
  hot u64;

  pub async do get(self, url u64) u64 {
    if url == hot {
      return 1u64;                        // answered here, at once
    }
    imm b = await net.read(url);
    b.size;                               // or here, once the reply came
  }
}

A reply always has an actor to go to: the request holds a counted handle of the actor that awaited, so that actor cannot die while its reply is owed. A behaviour that returns a value may also be sent with no await, and its value is then dropped.

Futures: storing a call's value#

A call of a behaviour that returns a value, written where its value is kept — bound to a local, passed on, returned — sends the request at once and gives back a future: a value standing for what the behaviour will return. An await on the future waits for that value. So several requests can be in flight at once, and their answers collected later:

actor Stock {
  pub async do price(self, item u64) u64 {item * 3u64;}
}

actor Basket {
  stock Stock;

  pub async do total(self) u64 {
    imm apples = stock.price(4u64);     // sent: a future comes back at once
    imm pears = stock.price(5u64);      // both requests are in flight
    imm a = await apples;               // waits for the first
    imm p = await pears;                // then for the second
    a + p;
  }
}

await basket.total() is 27. Both requests are sent before either is waited for, so Stock may work on the second while Basket waits for the first. A behaviour is declared once, and how it is called says what becomes of its value:

  • await stock.price(4u64) sends it and waits here: the await's value is the behaviour's, and no future is made.
  • imm f = stock.price(4u64) sends it and keeps its future, an actors.Future[u64], for an await later.
  • stock.price(4u64);, a statement of its own, sends it and forgets it: its value is dropped.

An await on a future pauses only if the value has not arrived. If it has, the behaviour goes on at once: nothing pauses, nothing waits in the actor, and none of the actor's other messages runs in between, so a future collected after other work is often free. If it has not, the behaviour pauses just as on a reply (What an await does), every borrow ending there, and resumes when the value arrives. Either way, what crosses the await is what crosses any await.

A future is a value like any other. It may be stored and awaited much later, and it may be handed to another actor in a message and awaited there: it belongs to whoever holds it, never to the actor that made the call, and whoever holds it may await it. It crosses to another thread where its value may: Future[T] is Sendable where T is. It copies or moves as its value does. Future[u64] copies, and each await on a copy gives the value again. A future of a value that moves, a Future[Report] where Report is a move type, moves too: the await takes the value out of it, and the future goes with it, so no second await could take the value again:

struct Report is Move {
  pub lines u64;
}

actor Printer {
  pub async do render(self, n u64) Report {new Report(n * 2u64);}
}

actor Archive {
  pub async do keep(self, f actors.Future[u64]) u64 {
    imm n = await f;                    // the value arrives here
    n + 1u64;
  }
}

actor Office {
  printer Printer;
  stock Stock;
  archive Archive;

  pub async do file(self) u64 {
    imm r = printer.render(10u64);      // a Future[Report], which moves
    imm report = await r;               // the Report is taken out of it
    report.lines;
  }

  pub async do pass(self) u64 {
    imm f = stock.price(2u64);
    await archive.keep(f);              // the future goes to Archive
  }
}

await office.file() is 20, and await office.pass() is 7. Awaiting r a second time is refused, as any use of a moved value is:

actor Desk {
  printer Printer;

  pub async do twice(self) u64 {
    imm r = printer.render(10u64);
    imm report = await r;
    imm again = await r;          // refused: r moved into the first 'await'
    report.lines;
  }
}
This variable's value has been moved out. It is no longer there to use.

A future nobody awaits is no error and costs nothing more: the behaviour runs, its value arrives, and it is finalized when the last holder of the future lets go, or when the value arrives, whichever is later. Letting go of a future does not stop its behaviour, which is already running on its own actor.

An await assumes the value arrives, as it nearly always does, and hands it over directly, with no wrapper to unwrap. When the behaviour owing it can never answer — its actor died while the behaviour was paused on something abandoned itself, as at the program's end — the future is abandoned. A behaviour already paused on it is abandoned with it, as one paused on the request itself would be: the rest of it never runs, and the values it held are finalized, in the order its one scope gives, once nothing else holds its actor. So the program's end, which ends what is outstanding, ends imm f = server.next(); await f as it ends await server.next(). An await that finds the future abandoned already panics, reported at the await:

actors: an awaited future will never arrive: the behaviour owing it (request 1) ended without answering

The request number counts the futures the program has made, so the report says which one it was. Where the failure is one the program should handle, the behaviour declares it in its return type, a Result, and the future's value is that Result.

Only a future, a behaviour's reply and an operation are awaited. An await on anything else waits for no answer, and is refused: an await on a function's call, whose value is there as soon as the call returns (below), or on a number. And a behaviour that returns nothing gives no future (next).

The compiler's --await-direct option, which is for testing the cut, runs the rest of the behaviour at once after an await on anything else, with the value awaited.

A behaviour that returns nothing#

Whether a reply is sent is the behaviour's declaration to say, never its caller's: one with no return type sends none, so there is nothing to await, and an await on it is refused. A caller that needs to know a behaviour has finished needs to know whether it worked, too: the behaviour declares a return type, a status, and the caller awaits that.

actor Log {
  pub async do flush(self) {}             // owes its caller nothing
  pub async do commit(self) bool {true;}  // answers with a status
}

actor Writer {
  log Log;
  saved bool;

  pub async do save(self) {
    await log.flush();            // refused: flush sends no reply
    saved = await log.commit();   // waits until commit has run, and learns whether it worked
  }
}
'await' waits for a behaviour's reply, and flush returns nothing, so it sends none: a
behaviour with no return type owes its caller nothing. If the caller needs to know flush
finished, declare a return type -- a status saying whether it worked -- and await that.

For the same reason its call gives no future: one whose value is kept is refused. Written as a statement of its own, it sends the message and forgets it.

actor Keeper {
  log Log;

  pub async do keep(self) {
    log.flush();                  // sent, and forgotten
    imm f = log.flush();          // refused: flush gives no future
  }
}
flush returns nothing, so its call gives no future to keep: a behaviour with no return
type owes its caller nothing, and calling it sends a message and forgets it. Call it as a
statement of its own; or, if the caller needs to know flush finished, declare a return
type -- a status saying whether it worked -- and keep or await that.

An acknowledgement is not flow control. A sender that awaits each reply before sending again has one message outstanding at a time, so the two run in lockstep, a round trip each. To keep a fast sender from running far ahead of a slow receiver while both stay busy, send through a bounded channel: its capacity is how far ahead the sender may get, and a send blocks only when it is full.

An await in a loop#

An await may stand in a loop, and a loop's variables cross it as any others do. It is legal, but poor practice:

actor Sizes {
  net Net;

  pub async do total(self, n u64) u64 {
    mut sum = 0u64;
    each i in 0u64 ..< n {
      imm b = await net.read(i);          // one request at a time
      sum = sum + b.size;
    }
    sum;
  }
}

Each pass sends one request and waits for its reply before the next is sent, so the requests run one after another, a round trip each, and the actor's other messages run in between the passes, perhaps changing what the loop reads. A borrow the loop holds ends at the await on every pass. Requests that do not depend on one another's replies need not wait in turn: each call's future kept, they are all in flight at once, and their values are awaited afterwards (Storing a future).

Not built yet#

Each of these is reported where it is written, as not built:

  • A few places an await may not cut yet: in the index of a place whose base is itself an expression holding an await ((await f()).items[await g()]), among the entries one <- appends that repeat or fill a value (v <- 3 of await f()), and in an array's contents that repeat a value more than 16 times (<- fill await f()). Written as a statement of its own first, each await is built.
  • Timeouts and cancellation. A paused behaviour waits until its reply comes. There is no time limit on the wait, and no way to cancel a paused behaviour. Only the program's end ends one early: a behaviour paused on I/O is abandoned then, the rest of it never run, and its values are finalized once nothing else holds its actor (Awaiting I/O); a behaviour whose reply was owed by one abandoned so is abandoned with it, and a future it owed is abandoned (Storing a future).
  • Awaiting several futures at once. An await waits for one future. Waiting for whichever of several arrives first, or for all of them in one await, is not built: several are awaited one after another.
  • Looking at a future without opening it. An await opens the future and panics if its value never arrives. There is no way yet to ask whether it arrived, or to handle an abandoned one, without the panic.

One of them, beside an await refused for what it awaits, where compute is a plain function:

actor Waiter {
  clock Clock;
  last u64;

  pub async do onFunction(self) {
    last = await compute();       // refused: compute's u64 is no future
  }

  pub async do repeated(self) {
    mut v = List[u64].empty();
    v <- 3 of await clock.now();  // not built: among the entries a '<-' appends
  }
}