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
-
async/await syntax - plan of attack
by Paul "LeoNerd" Evans