Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First-class global macros #2511

Merged
merged 5 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
Unreleased
=============================

Removals
------------------------------
* `delmacro` has been removed. Use `(del (get _hy_macros (hy.mangle
…)))` instead.
* `hy.reserved` has been removed. Use `(.keys (builtins._hy_macros))`
or Python's built-in `keyword` module instead.

Breaking Changes
------------------------------

* `defmacro` and `require` can now define macros locally instead of
only module-wide.

* `hy.eval`, `hy.macroexpand`, `doc`, and `delmacro` don't work with
* `hy.eval`, `hy.macroexpand`, and `doc` don't work with
local macros (yet).

* When a macro is `require`\d from another module, that module is no
Expand Down
6 changes: 0 additions & 6 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1405,9 +1405,3 @@ Python Operators

.. hy:automodule:: hy.pyops
:members:

Reserved
--------

.. hy:automodule:: hy.reserved
:members:
17 changes: 17 additions & 0 deletions docs/macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
Macros
======

Operations on macros
--------------------

The currently defined global macros can be accessed as a dictionary ``_hy_macros`` in each module. (Use ``bulitins._hy_macros``, attached to Python's usual :py:mod:`builtins` module, to see core macros.) The keys are mangled macro names and the values are the function objects that implement the macros. You can operate on this dictionary to list, add, delete, or get help on macros, but be sure to use :hy:func:`eval-and-compile` or :hy:func:`eval-when-compile` when you need the effect to happen at compile-time. ::

(defmacro m []
"This is a docstring."
`(print "Hello, world."))
(print (in "m" _hy_macros)) ; => True
(help (get _hy_macros "m"))
(m) ; => "Hello, world."
(eval-and-compile
(del (get _hy_macros "m")))
(m) ; => NameError

``_hy_reader_macros`` is a similar dictionary for reader macros, but here, the keys aren't mangled.

.. _using-gensym:

Using gensym for Safer Macros
Expand Down
41 changes: 6 additions & 35 deletions hy/core/macros.hy
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,16 @@

With no arguments, ``cond`` returns ``None``. With an odd number of
arguments, ``cond`` raises an error."
(defn _cond [args]
(if args
`(if ~(get args 0)
~(get args 1)
~(_cond (cut args 2 None)))
'None))
(if (% (len args) 2)
(raise (TypeError "`cond` needs an even number of arguments"))
(_cond args)))

(defn _cond [args]
(if args
`(if ~(get args 0)
~(get args 1)
~(_cond (cut args 2 None)))
'None))


(defmacro when [test #* body]
#[[Shorthand for ``(if test (do …) None)``. See :hy:func:`if`. For a logically negated version, see Hyrule's :hy:func:`unless <hyrule.control.unless>`.
Expand Down Expand Up @@ -166,31 +165,3 @@
(if (isinstance a hy.models.List)
(lfor x a (hy.models.String x))
(raise (TypeError "arguments must be keywords or lists of symbols"))))))))

(defmacro delmacro
[#* names]
#[[Delete a macro(s) from the current module. This doesn't work on a
local macro.
::

=> (require a-module [some-macro])
=> (some-macro)
1

=> (delmacro some-macro)
=> (some-macro)
Traceback (most recent call last):
File "<string>", line 1, in <module>
(some-macro)
NameError: name 'some_macro' is not defined

=> (delmacro some-macro)
Traceback (most recent call last):
File "<string>", line 1, in <module>
(delmacro some-macro)
NameError: macro 'some-macro' is not defined
]]
(let [sym (hy.gensym)]
`(eval-and-compile
(for [~sym ~(lfor name names (hy.mangle name))]
(when (in ~sym _hy_macros) (del (get _hy_macros ~sym)))))))
33 changes: 0 additions & 33 deletions hy/reserved.hy

This file was deleted.

19 changes: 0 additions & 19 deletions tests/native_tests/macros.hy
Original file line number Diff line number Diff line change
Expand Up @@ -168,25 +168,6 @@
(assert (in "HyWrapperError" (str excinfo.value))))


(defmacro delete-me [] "world")

(defn test-delmacro
[]
;; test deletion of user defined macro
(delmacro delete-me)
(with [exc (pytest.raises NameError)]
(delete-me))
;; test deletion of required macros
(require tests.resources.tlib [qplah parald])
(assert (and (qplah 1) (parald 1)))

(delmacro qplah parald)
(with [exc (pytest.raises NameError)]
(hy.eval '(qplah)))
(with [exc (pytest.raises NameError)]
(hy.eval '(parald))))


(defn macro-redefinition-warning-tester [local]
(for [should-warn? [True False] head ["defmacro" "require"]]
(with [(if should-warn?
Expand Down
60 changes: 60 additions & 0 deletions tests/native_tests/macros_first_class.hy
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"Tests of using macros as first-class objects: listing, creating, and
deleting them, and retrieving their docstrings."

(import
builtins)


(defn test-builtins []
(assert (in "when" (.keys builtins._hy_macros)))
(assert (not-in "global1" (.keys builtins._hy_macros)))
(assert (not-in "nonexistent" (.keys builtins._hy_macros)))

(setv s (. builtins _hy_macros ["when"] __doc__))
(assert s)
(assert (is (type s) str)))


; There are three ways to define a global macro:
; 1. `defmacro` in global scope
(defmacro global1 []
"global1 docstring"
"from global1")
; 2. `require` in global scope
(require tests.resources.tlib [qplah :as global2])
; 3. Manually updating `_hy_macros`
(eval-and-compile (setv (get _hy_macros "global3") (fn [&compiler]
"from global3")))
(eval-and-compile (setv (get _hy_macros (hy.mangle "global☘")) (fn [&compiler]
"global☘ docstring"
"from global☘")))

(defn test-globals []
(assert (not-in "when" (.keys _hy_macros)))
(assert (not-in "nonexistent" (.keys _hy_macros)))
(assert (all (gfor
k ["global1" "global2" "global3" "global☘"]
(in (hy.mangle k) (.keys _hy_macros)))))
(assert (= (global3) "from global3"))
(assert (= (global☘) "from global☘"))
(assert (=
(. _hy_macros ["global1"] __doc__)
"global1 docstring"))
; https://github.com/hylang/hy/issues/1946
(assert (=
(. _hy_macros [(hy.mangle "global☘")] __doc__)
"global☘ docstring")))

; Try creating and and then deleting a global macro.
(defn global4 []
"from global4 function")
(setv global4-f1 (global4)) ; Calls the function
(eval-and-compile (setv (get _hy_macros "global4") (fn [&compiler]
"from global4 macro")))
(setv global4-m (global4)) ; Calls the macro
(eval-and-compile (del (get _hy_macros "global4")))
(setv global4-f2 (global4)) ; Calls the function again

(defn test-global-delete []
(assert (= (global4) global4-f1 global4-f2 "from global4 function"))
(assert (= global4-m "from global4 macro")))
18 changes: 0 additions & 18 deletions tests/native_tests/reserved.hy

This file was deleted.