obriencj / python-sibilant

A dialect of Lisp compiling to Python bytecode
GNU Lesser General Public License v3.0
7 stars 1 forks source link

Add support for coroutines #228

Closed ahills closed 4 years ago

ahills commented 4 years ago

Using the await special, a function becomes a coroutine suitable for use with Python's asyncio library.

CPython actually produces a YIELD_FROM opcode for await, which is mysteriously always (¿?) preceded by pushing a None value that is sent to the generator, coroutine, or iterator returned by GET_AWAITABLE or GET_YIELD_FROM_ITER[0]. With this patch, both the new await special and the yield-from special get a send: keyword argument that allows sending values other than None with YIELD_FROM. This doesn't appear to be defined behavior, so expect surprises.

[0] https://github.com/python/cpython/blob/b54a99d6432de93de85be2b42a63774f8b4581a0/Python/ceval.c#L2157

obriencj commented 4 years ago

This produces warnings under 3.5 and 3.6, and fails on 3.7 due to the name of some attributes. Apparently async and await are now reserved, so cannot be member, variable, nor parameter names.

ahills commented 4 years ago

After inspecting the behavior of CPython 3.6 & 3.8 more closely, I think this PR misses a major feature by eliding the async keyword: it's not possible to create a coroutine without using await, whereas CPython creates a coroutine from an async function that never awaits.

In other words, it's not possible with this implementation to create either x() or y() below:

async def x():
    return 1

async def y():
    yield 1

The former has the COROUTINE flag, and the latter has the ASYNC_GENERATOR flag (which I completely missed). An expression invoking a regular function will return a regular value instead of an awaitable, so can't be used with the await keyword.

I propose creating new coroutine definition facilities: (function …)(async …)/(coroutine …), (def function …)(def async …)/(def coroutine …), (defun …)(defasync …)/(defcoro[utine] …)—any preference on names?

It might also be valuable to add an async decorator to flag a function or generator as async, i.e., (define coro (async (function …))) & (define agen (async (#gen …))). I'm not sure this would be as easy to implement elegantly, and is probably not as useful.

obriencj commented 4 years ago

I like “coroutine” or “async-function” both. Any ideas for lambda? “async-lambda” or “coro” ?

obriencj commented 4 years ago

I was also thinking that a macro like (declare-async) could be used to expressly set the coroutine flag in the current compiler. That would make it so defasync or coroutine could simply be macros instead of specials (ie. less python code, more sibilant code!)

obriencj commented 4 years ago

Was also thinking about the current implementation of let, maybe we should allow it to become a coroutine if await is used within it, and then check at the invocation of the let lambda to await rather than call it?

obriencj commented 4 years ago

(That can all come later, too, if you want to use this PR as-is)

obriencj commented 4 years ago

Did you want to merge this as-is and worry about other tweaks later?

ahills commented 4 years ago

No, I think the current design is wrong. It's important to be able to define coroutines without using the await keyword in them. I have a patch that adds the following macros & specials:

The last one is a little strange, because it's not immediately obvious that the let implementation uses functions; if you're invoking it directly, you either need to be awaiting the result, or handing it off to one of the asyncio methods that handles awaitables. I am hoping that invoking the POSIX bourne shell syntax for asynchronous jobs indicates that you'll get something other than a normal synchronous return value.

Right now the patch's tests are incomplete, so I haven't pushed the changes up.