develooper Front page | perl.perl5.porters | Postings from July 2016

async/await syntax - plan of attack

Thread Next
From:
Paul "LeoNerd" Evans
Date:
July 1, 2016 21:15
Subject:
async/await syntax - plan of attack
Message ID:
20160701221525.19581d92@shy.leonerd.org.uk
((This is the second of a long three-part mail series. This outlines my
  current attempt at a solution, and where it has failed. In the third
  part I'll explain what I would find useful assistance from p5p.

  If you get bored reading this, just skip to the third.))

In order to implement this syntax in Perl, I have attempted to write
a module, Future::AsyncAwait, that provides the necessary keywords and
semantics behind them.

I think the notation first has to be altered a little. Rather than
hooking a lot of awkward parser state, it would be far easier if
'await' was simply a code attribute; thus one actually writes

  use Future::AsyncAwait;

  sub outer :Async {
    return await inner();
  }

The basic plan involves creating a new pair of ops, ENTERASYNC and
LEAVEASYNC to be spliced into the optree of every :Async marked
function, and an AWAIT op that implements the 'await' keyword.

The operation of these ops should be "relatively obvious" if they don't
ever encounter a pending Future. They should be as transparent as
possible, simply boxing or unboxing results or exceptions as Future
instances.

When they encounter a pending future, the idea is to suspend the body of
the currently-executing :Async function by cloning its optree and
runtime state into a new CV and causing the original one to return.
This cloned CV can then be resumed later on. Because we're working
entirely within Perl's own CVs, we don't have to do any horrible
C-stack work or other trickery that plague other similar attempts to
create coroutines, because the granularity of the suspend-and-resume
operation is entirely within a single sub.

My approximate idea for how to implement this plan is as follows. Note
this is as yet still a vague handwavy plan, as I've not been able to get
very far into it for reasons that will become apparent.

 1) Perform an optree rewrite as a side-effect of marking :Async on
    a CODE block, turning
     oproot = LEAVESUB(BODY...)
    into
     oproot = LEAVESUB(LEAVEASYNC(ENTERASYNC, BODY...))

    The job of LEAVEASYNC is to inspect the circumstances of the
    function's exit (either return or die) and construct a new Future
    instance ((or reuse - see below)) to contain that result, before
    putting it on the stack for the caller to see.

    The job of ENTERASYNC is simply to push a new level onto the
    context stack so that return and die can find the appropriate
    LEAVEASYNC.

 2) Make 'await' as a callchecked function. The checker can then
    perform such tasks as ensuring it only appears inside an ':Async'
    function, and replace its invocation with an AWAIT custom op.

    The job of AWAIT is to evaluate its argument, and check that
    it receives a Future instance (what it does if it isn't is still
    yet to be designed ;) )

    If that instance is not pending, then the result is fetched by
    invoking the 'get' method on it, thus implementing the behaviour
    responsible for making
      my $x = await Future->done("val");
    behave like
      my $x = Future->done("val")->get;

    At this point the execution of the code can proceed as normal into
    the next sibling of the AWAIT.

 4) The interesting part then comes in what happens when AWAIT
    encounters a pending Future.

    To handle this we create a new CV that shares the same optree as
    the currently-executing (:Async-marked) function and steal the
    current PAD into it. The 'oproot' is the same, but the 'opstart'
    points at the currently-executing AWAIT op.

    A new Future instance is then allocated and a reference to it is
    stored in a special slot (maybe using magic) of this new CV. We
    arrange for this CV to be invoked when the (original) pending
    Future completes.

    The await'ing function then immediately returns to its caller,
    returning a reference to this newly-created and still-pending
    Future instance.

    At some later time, when the originally-encountered future now
    completes, it invokes the CV. Because 'opstart' points directly
    into the AWAIT op, the AWAIT resumes with its normal operation,
    simply fetching the result from the future we now know isn't
    pending, and lets the CV continue to run through its optree, with
    the saved values from the PAD.

 5) Any subsequent AWAITs within the code that also have to suspend
    it can observe that it already has a CV to use, so this part can
    be skipped. The 'opstart' simply needs updating to point at this
    current AWAIT op and then the code can suspend itself as before.

 6) Eventually, this resumed CV will probably return or die, thus
    invoking the wrapping LEAVEASYNC we put into the optree. This one
    now needs to notice that it is operating on a CV that had already
    constructed a Future instance, so all it needs to do is set the
    result value of that directly; it does not need to allocate a new
    one. It can then destroy the PAD and the containing CV.

The more astute readers who are still with me at this point may observe
that this plan only works at toplevel expression statements, that
appear in void context, outside of any while or for loops, or other
weird situations.

Indeed so.

Therefore, the AWAIT op has to perform somewhat more work here.
Whenever there are stack temporaries it must save those too so it can
restore them on resume; i.e. it has to have a place to store the value
"saved" in the following code

  sub :Async { return "saved" . await foo() }

Additionally, it has to be able to inspect the context stack so that
any intervening loops or nested eval blocks can themselves be saved and
restored later on. This becomes particularly fun in the case of

  sub :Async {
    my @f = ( Future->done(1), foo(), Future->done(3) );
    foreach my $f (@f) {
      await $f;
    }
  }

At the time that 'await' is suspending execution because it's waiting
for the future that foo() returned to complete, we have to remember
where in the @f list we got up to, so we can continue from that point.

All in all then, it can be observed that this plan involves lots of
poking a perl's internals from an XS module. This is in part where so
far I've not been able to make much progress. In particular, my current
implementation of ENTERASYNC/LEAVEASYNC has been frustrated by my
initial idea of copying perl's own ENTERTRY/LEAVETRY and adjusting them
to suit my purposes. This doesn't work because perl's own ops mostly
just call internal static functions, that I can't see from an XS
context.

I'm currently stuck for how to mutate the optree in a CV so that my own
inserted code can influence its return value - a feature I need in
order to make :Async marked functions return Future instances.

-- 
Paul "LeoNerd" Evans

leonerd@leonerd.org.uk      |  https://metacpan.org/author/PEVANS
http://www.leonerd.org.uk/  |  https://www.tindie.com/stores/leonerd/

Thread Next


nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About