Skip to content

Commit

Permalink
Merge pull request #2548 from Kodiologist/macollapse
Browse files Browse the repository at this point in the history
Collapse some similar core macros
  • Loading branch information
Kodiologist committed Jan 23, 2024
2 parents 120041f + b885cfd commit 9206b5c
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 168 deletions.
12 changes: 12 additions & 0 deletions NEWS.rst
Expand Up @@ -3,6 +3,18 @@
Unreleased
=============================

Removals
------------------------------
* `(defn/a …)` is now `(defn :async …)`.
* `(fn/a …)` is now `(fn :async …)`.
* `(with/a […] …)` is now `(with [:async …] …)`.

* As with `for`, `:async` must precede each name to be bound
asynchronously, because you can mix synchronous and asynchronous
types.

* `(yield-from …)` is now `(yield :from …)`.

New Features
------------------------------
* You can now set `repl-ps1` and `repl-ps2` in your `HYSTARTUP` to customize
Expand Down
65 changes: 25 additions & 40 deletions docs/api.rst
Expand Up @@ -91,13 +91,7 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
allowed), and the newly created function object is returned. Decorators and
type parameters aren't allowed, either. However, the function body is
understood identically to that of :hy:func:`defn`, without any of the
restrictions of Python's :py:keyword:`lambda`. See :hy:func:`fn/a` for the
asynchronous equivalent.

.. hy:macro:: (fn/a [name #* args])
As :hy:func:`fn`, but the created function object will be a :ref:`coroutine
<py:async def>`.
restrictions of Python's :py:keyword:`lambda`. ``:async`` is also allowed.

.. hy:macro:: (defn [name #* args])
Expand All @@ -117,16 +111,16 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
5))``. There is one exception: due to Python limitations, no implicit return
is added if the function is an asynchronous generator (i.e., defined with
:hy:func:`defn/a` or :hy:func:`fn/a` and containing at least one
:hy:func:`yield` or :hy:func:`yield-from`).

``defn`` accepts a few more optional arguments: a bracketed list of
:term:`decorators <py:decorator>`, a list of type parameters (see below),
and an annotation (see :hy:func:`annotate`) for the return value. These are
placed before the function name (in that order, if several are present)::
:hy:func:`yield`).

(defn [decorator1 decorator2] :tp [T1 T2] #^ annotation name [params] …)
``defn`` accepts a few more optional arguments: a literal keyword ``:async``
(to create a :ref:`coroutine <py:async def>` like Python's ``async def``), a
bracketed list of :term:`decorators <py:decorator>`, a list of type
parameters (see below), and an annotation (see :hy:func:`annotate`) for the
return value. These are placed before the function name (in that order, if
several are present)::

To define asynchronous functions, see :hy:func:`defn/a` and :hy:func:`fn/a`.
(defn :async [decorator1 decorator2] :tp [T1 T2] #^ annotation name [params] …)

``defn`` lambda lists support all the same features as Python parameter
lists and hence are complex in their full generality. The simplest case is a
Expand Down Expand Up @@ -174,11 +168,6 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
an unpacked symbol (such as ``#* T`` or ``#** T``). As in Python, unpacking
and annotation can't be used with the same parameter.

.. hy:macro:: (defn/a [name lambda-list #* body])
As :hy:func:`defn`, but defines a :ref:`coroutine <py:async def>` like
Python's ``async def``.

.. hy:macro:: (defmacro [name lambda-list #* body])
``defmacro`` is used to define macros. The general format is
Expand Down Expand Up @@ -1206,10 +1195,10 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.

.. hy:macro:: (with [managers #* body])
``with`` compiles to a :py:keyword:`with` statement, which wraps some code
with one or more :ref:`context managers <py:context-managers>`. The first
argument is a bracketed list of context managers, and the remaining
arguments are body forms.
``with`` compiles to a :py:keyword:`with` or an :py:keyword:`async with`
statement, which wraps some code with one or more :ref:`context managers
<py:context-managers>`. The first argument is a bracketed list of context
managers, and the remaining arguments are body forms.

The manager list can't be empty. If it has only one item, that item is
evaluated to obtain the context manager to use. If it has two, the first
Expand All @@ -1236,41 +1225,37 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
Python's ``with e1 as _: …``), ``with`` will leave it anonymous (as Python's
``with e1: …``).

Finally, any variable-manager pair may be preceded with the keyword
``:async`` to use an asynchronous context manager::

(with [:async v1 e1] …)

``with`` returns the value of its last form, unless it suppresses an
exception (because the context manager's ``__exit__`` method returned true),
in which case it returns ``None``. So, the previous example could also be
in which case it returns ``None``. So, the first example could also be
written ::

(print (with [o (open "file.txt" "rt")] (.read o)))

.. hy:macro:: (with/a [managers #* body])
As :hy:func:`with`, but compiles to an :py:keyword:`async with` statement.

.. hy:macro:: (yield [value])
.. hy:macro:: (yield [arg1 arg2])
``yield`` compiles to a :ref:`yield expression <py:yieldexpr>`, which
returns a value as a generator. As in Python, one argument, the value to
yield, is accepted, and it defaults to ``None``. ::
returns a value as a generator. For a plain yield, provide one argument,
the value to yield, or omit it to yield ``None``. ::

(defn naysayer []
(while True
(yield "nope")))
(hy.repr (list (zip "abc" (naysayer))))
; => [#("a" "nope") #("b" "nope") #("c" "nope")]

For ``yield from``, see :hy:func:`yield-from`.

.. hy:macro:: (yield-from [object])
``yield-from`` compiles to a :ref:`yield-from expression <py:yieldexpr>`,
which returns a value from a subgenerator. The syntax is the same as that of
:hy:func:`yield`. ::
For a yield-from expression, provide two arguments, where the first is the
literal keyword ``:from`` and the second is the subgenerator. ::

(defn myrange []
(setv r (range 10))
(while True
(yield-from r)))
(yield :from r)))
(hy.repr (list (zip "abc" (myrange))))
; => [#("a" 0) #("b" 1) #("c" 2)]

Expand Down
85 changes: 51 additions & 34 deletions hy/core/result_macros.py
Expand Up @@ -1068,21 +1068,15 @@ def compile_break_or_continue_expression(compiler, expr, root):
# ------------------------------------------------


@pattern_macro(
["with", "with/a"],
[
brackets(oneplus(FORM + FORM))
| brackets(FORM >> (lambda x: [(Symbol("_"), x)])),
many(FORM),
],
)
@pattern_macro("with", [
brackets(oneplus(maybe(keepsym(":async")) + FORM + FORM)) |
brackets((maybe(keepsym(":async")) + FORM) >> (lambda x: [(x[0], Symbol("_"), x[1])])),
many(FORM)])
def compile_with_expression(compiler, expr, root, args, body):
body = compiler._compile_branch(body)

# Store the result of the body in a tempvar
# We'll store the result of the body in a tempvar
temp_var = compiler.get_anon_var()
name = asty.Name(expr, id=mangle(temp_var), ctx=ast.Store())
body += asty.Assign(expr, targets=[name], value=body.force_expr)
# Initialize the tempvar to None in case the `with` exits
# early with an exception.
initial_assign = asty.Assign(
Expand All @@ -1091,7 +1085,18 @@ def compile_with_expression(compiler, expr, root, args, body):

ret = Result(stmts=[initial_assign])
items = []
for variable, ctx in args[0]:
was_async = None
cbody = None
for i, (is_async, variable, ctx) in enumerate(args[0]):
is_async = bool(is_async)
if was_async is None:
was_async = is_async
elif is_async != was_async:
# We're compiling a `with` that mixes synchronous and
# asynchronous context managers. Python doesn't support
# this directly, so start a new `with` inside the body.
cbody = compile_with_expression(compiler, expr, root, [args[0][i:]], body)
break
ctx = compiler.compile(ctx)
ret += ctx
variable = (
Expand All @@ -1103,8 +1108,12 @@ def compile_with_expression(compiler, expr, root, args, body):
asty.withitem(expr, context_expr=ctx.force_expr, optional_vars=variable)
)

node = asty.With if root == "with" else asty.AsyncWith
ret += node(expr, body=body.stmts, items=items)
if not cbody:
cbody = compiler._compile_branch(body)
cbody += asty.Assign(expr, targets=[name], value=cbody.force_expr)

node = asty.AsyncWith if was_async else asty.With
ret += node(expr, body=cbody.stmts, items=items)

# And make our expression context our temp variable
expr_name = asty.Name(expr, id=mangle(temp_var), ctx=ast.Load())
Expand Down Expand Up @@ -1454,10 +1463,12 @@ def compile_try_expression(compiler, expr, root, body, catchers, orelse, finalbo
)


@pattern_macro(["fn", "fn/a"],
[maybe(type_params), maybe_annotated(lambda_list), many(FORM)])
def compile_function_lambda(compiler, expr, root, tp, params, body):
is_async = root == "fn/a"
@pattern_macro("fn", [
maybe(keepsym(":async")),
maybe(type_params),
maybe_annotated(lambda_list),
many(FORM)])
def compile_function_lambda(compiler, expr, root, is_async, tp, params, body):
params, returns = params
posonly, args, rest, kwonly, kwargs = params
has_annotations = returns is not None or any(
Expand All @@ -1483,12 +1494,14 @@ def compile_function_lambda(compiler, expr, root, tp, params, body):
return ret + Result(expr=ret.temp_variables[0])


@pattern_macro(
["defn", "defn/a"],
[maybe(brackets(many(FORM))), maybe(type_params), maybe_annotated(SYM), lambda_list, many(FORM)],
)
def compile_function_def(compiler, expr, root, decorators, tp, name, params, body):
is_async = root == "defn/a"
@pattern_macro("defn", [
maybe(keepsym(":async")),
maybe(brackets(many(FORM))),
maybe(type_params),
maybe_annotated(SYM),
lambda_list,
many(FORM)])
def compile_function_def(compiler, expr, root, is_async, decorators, tp, name, params, body):
name, returns = name
node = asty.AsyncFunctionDef if is_async else asty.FunctionDef
decorators, ret, _ = compiler._compile_collect(decorators[0] if decorators else [])
Expand Down Expand Up @@ -1672,23 +1685,27 @@ def compile_return(compiler, expr, root, arg):
return ret + asty.Return(expr, value=ret.force_expr)


@pattern_macro("yield", [maybe(FORM)])
def compile_yield_expression(compiler, expr, root, arg):
@pattern_macro("yield", [times(0, 2, FORM)])
def compile_yield_expression(compiler, expr, root, args):
if is_inside_function_scope(compiler.scope):
nearest_python_scope(compiler.scope).has_yield = True
yield_from = False
if len(args) == 2:
from_kw, x = args
if from_kw != Keyword("from"):
raise compiler._syntax_error(from_kw, "two-argument `yield` requires `:from`")
yield_from = True
args = [x]
ret = Result()
if arg is not None:
ret += compiler.compile(arg)
return ret + asty.Yield(expr, value=ret.force_expr)
if args:
ret += compiler.compile(args[0])
return ret + (asty.YieldFrom if yield_from else asty.Yield)(expr, value=ret.force_expr)


@pattern_macro(["yield-from", "await"], [FORM])
@pattern_macro("await", [FORM])
def compile_yield_from_or_await_expression(compiler, expr, root, arg):
if root == "yield-from" and is_inside_function_scope(compiler.scope):
nearest_python_scope(compiler.scope).has_yield = True
ret = Result() + compiler.compile(arg)
node = asty.YieldFrom if root == "yield-from" else asty.Await
return ret + node(expr, value=ret.force_expr)
return ret + asty.Await(expr, value=ret.force_expr)


# ------------------------------------------------
Expand Down
5 changes: 3 additions & 2 deletions tests/compilers/test_ast.py
Expand Up @@ -550,11 +550,12 @@ def test_compiler_macro_tag_try():


def test_ast_good_yield_from():
can_compile("(yield-from [1 2])")
can_compile("(yield :from [1 2])")
can_compile("(yield :from)")


def test_ast_bad_yield_from():
cant_compile("(yield-from)")
cant_compile("(yield :ploopy [1 2])")


def test_eval_generator_with_return():
Expand Down
8 changes: 4 additions & 4 deletions tests/native_tests/comprehensions.hy
Expand Up @@ -387,25 +387,25 @@


(defn [async-test] test-for-async []
(defn/a numbers []
(defn :async numbers []
(for [i [1 2]]
(yield i)))

(asyncio.run
((fn/a []
((fn :async []
(setv x 0)
(for [:async a (numbers)]
(setv x (+ x a)))
(assert (= x 3))))))


(defn [async-test] test-for-async-else []
(defn/a numbers []
(defn :async numbers []
(for [i [1 2]]
(yield i)))

(asyncio.run
((fn/a []
((fn :async []
(setv x 0)
(for [:async a (numbers)]
(setv x (+ x a))
Expand Down
6 changes: 3 additions & 3 deletions tests/native_tests/decorators.hy
Expand Up @@ -53,10 +53,10 @@
(assert (= l ["dec" "arg" "foo" "foo fn" "bar body" 1])))


(defn [async-test] test-decorated-defn/a []
(defn decorator [func] (fn/a [] (/ (await (func)) 2)))
(defn [async-test] test-decorated-defn-a []
(defn decorator [func] (fn :async [] (/ (await (func)) 2)))

(defn/a [decorator] coro-test []
(defn :async [decorator] coro-test []
(await (asyncio.sleep 0))
42)
(assert (= (asyncio.run (coro-test)) 21)))

0 comments on commit 9206b5c

Please sign in to comment.