From e588b4928dfbe9af72cef942638ac686b72b3cf5 Mon Sep 17 00:00:00 2001 From: Tuukka Turto Date: Thu, 15 Dec 2016 02:10:46 +0200 Subject: [PATCH] add defmacro! and fix macro expansion error message (#1172) * added defmacro! * revert #924 #924 had an error and should never have been merged in the first place. (see #903) * put back import getargspec Without the `formatargspec` this time. * Give better error message on failed macro expansion Better error messages work most of the time. In cases where there are parameters that aren't valid in Python, error message shown is rather ugly. But this is better than no error messages at all and such macros with strange parameter names are rather rare. * fix flake8 errors * Minor English improvements --- docs/language/api.rst | 29 +++++++++++++++++++++++ hy/core/macros.hy | 8 +++++++ hy/macros.py | 33 ++++++++++++++++++++------ tests/native_tests/native_macros.hy | 36 +++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) diff --git a/docs/language/api.rst b/docs/language/api.rst index 1c5a2ec..5838bc2 100644 --- a/docs/language/api.rst +++ b/docs/language/api.rst @@ -696,6 +696,35 @@ For example, ``g!a`` would become ``(gensym "a")``. Section :ref:`using-gensym` +.. _defmacro!: + +defmacro! +--------- + +``defmacro!`` is like ``defmacro/g!`` plus automatic once-only evaluation for +``o!`` parameters, which are available as the equivalent ``g!`` symbol. + +For example, + +.. code-block:: clj + + => (defn expensive-get-number [] (print "spam") 14) + => (defmacro triple-1 [n] `(+ n n n)) + => (triple-1 (expensive-get-number)) ; evals n three times + spam + spam + spam + 42 + => (defmacro/g! triple-2 [n] `(do (setv ~g!n ~n) (+ ~g!n ~g!n ~g!n))) + => (triple-2 (expensive-get-number)) ; avoid repeats with a gensym + spam + 42 + => (defmacro! triple-3 [o!n] `(+ ~g!n ~g!n ~g!n)) + => (triple-3 (expensive-get-number)) ; easier with defmacro! + spam + 42 + + defreader --------- diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 357687c..b41b45a 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -221,6 +221,14 @@ (let ~gensyms ~@body)))) +(defmacro defmacro! [name args &rest body] + "Like defmacro/g! plus automatic once-only evaluation for o! + parameters, which are available as the equivalent g! symbol." + (setv os (list-comp s [s args] (.startswith s "o!")) + gs (list-comp (HySymbol (+ "g!" (cut s 2))) [s os])) + `(defmacro/g! ~name ~args + `(do (setv ~@(interleave ~gs ~os)) + ~@~body))) (if-python2 (defmacro/g! yield-from [expr] diff --git a/hy/macros.py b/hy/macros.py index e59f760..02b1330 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -52,8 +52,14 @@ def macro(name): """ def _(fn): - argspec = getargspec(fn) - fn._hy_macro_pass_compiler = argspec.keywords is not None + try: + argspec = getargspec(fn) + fn._hy_macro_pass_compiler = argspec.keywords is not None + except Exception: + # An exception might be raised if fn has arguments with + # names that are invalid in Python. + fn._hy_macro_pass_compiler = False + module_name = fn.__module__ if module_name.startswith("hy.core"): module_name = None @@ -140,12 +146,24 @@ def load_macros(module_name): def make_empty_fn_copy(fn): - argspec = getargspec(fn) - formatted_args = formatargspec(*argspec) - fn_str = 'lambda {}: None'.format( - formatted_args.lstrip('(').rstrip(')')) + try: + # This might fail if fn has parameters with funny names, like o!n. In + # such a case, we return a generic function that ensures the program + # can continue running. Unfortunately, the error message that might get + # raised later on while expanding a macro might not make sense at all. + + argspec = getargspec(fn) + formatted_args = formatargspec(*argspec) + + fn_str = 'lambda {}: None'.format( + formatted_args.lstrip('(').rstrip(')')) + empty_fn = eval(fn_str) + + except Exception: + + def empty_fn(*args, **kwargs): + None - empty_fn = eval(fn_str) return empty_fn @@ -194,6 +212,7 @@ def macroexpand_1(tree, compiler): msg = "expanding `" + str(tree[0]) + "': " msg += str(e).replace("()", "", 1).strip() raise HyMacroExpansionError(tree, msg) + try: obj = wrap_value(m(*ntree[1:], **opts)) except HyTypeError as e: diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy index 81b485f..2d0b19e 100644 --- a/tests/native_tests/native_macros.hy +++ b/tests/native_tests/native_macros.hy @@ -202,6 +202,42 @@ (setv macro2 "(defmacro/g! two-point-zero [] `(+ (float 1) 1.0))") (assert (import_buffer_to_ast macro2 "foo"))) +(defn test-defmacro! [] + ;; defmacro! must do everything defmacro/g! can + (import ast) + (import [astor.codegen [to_source]]) + (import [hy.importer [import_buffer_to_ast]]) + (setv macro1 "(defmacro! nif [expr pos zero neg] + `(let [~g!res ~expr] + (cond [(pos? ~g!res) ~pos] + [(zero? ~g!res) ~zero] + [(neg? ~g!res) ~neg]))) + + (print (nif (inc -1) 1 0 -1)) + ") + ;; expand the macro twice, should use a different + ;; gensym each time + (setv _ast1 (import_buffer_to_ast macro1 "foo")) + (setv _ast2 (import_buffer_to_ast macro1 "foo")) + (setv s1 (to_source _ast1)) + (setv s2 (to_source _ast2)) + (assert (in ":res_" s1)) + (assert (in ":res_" s2)) + (assert (not (= s1 s2))) + + ;; defmacro/g! didn't like numbers initially because they + ;; don't have a startswith method and blew up during expansion + (setv macro2 "(defmacro! two-point-zero [] `(+ (float 1) 1.0))") + (assert (import_buffer_to_ast macro2 "foo")) + + (defmacro! foo! [o!foo] `(do ~g!foo ~g!foo)) + ;; test that o! becomes g! + (assert (= "Hy" (foo! "Hy"))) + ;; test that o! is evaluated once only + (setv foo 40) + (foo! (+= foo 1)) + (assert (= 41 foo))) + (defn test-if-not [] (assert (= (if-not True :yes :no)